From f124f8d881d76294be6517cf1449c1b6c9328935 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 05:44:27 -0700 Subject: [PATCH 001/102] Implement the maximum link rate fix for some laptops with 4K display. --- WhateverGreen/kern_igfx.cpp | 102 +++++++++++++++++++++++++++++++++++- WhateverGreen/kern_igfx.hpp | 66 ++++++++++++++++++++++- 2 files changed, 166 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 5607897c..8a47ca78 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -159,7 +159,36 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if (checkKernelArgument("-igfxfbdump")) dumpPlatformTable = true; #endif - + + // Enable maximum link rate patch if the corresponding boot argument is found + if (checkKernelArgument("-igfxmlr")) + maxLinkRatePatch = true; + else + // Or if "enable-dpcd-max-link-rate-fix" is set in IGPU property + WIOKit::getOSDataValue(info->videoBuiltin, "enable-dpcd-max-link-rate-fix", maxLinkRatePatch); + + // Read the custom maximum link rate if present + if (WIOKit::getOSDataValue(info->videoBuiltin, "dpcd-max-link-rate", maxLinkRate)) { + // Guard: Verify the custom link rate before using it + switch (maxLinkRate) { + case 0x1E: // HBR3 8.1 Gbps + case 0x14: // HBR2 5.4 Gbps + case 0x0C: // 3_24 3.24 Gbps Used by Apple internally + case 0x0A: // HBR 2.7 Gbps + case 0x06: // RBR 1.62 Gbps + DBGLOG("igfx", "MLR: Found a valid custom maximum link rate value 0x%02x", maxLinkRate); + break; + + default: + // Invalid link rate value + SYSLOG("igfx", "MLR: Found an invalid custom maximum link rate value. Will use 0x14 as a fallback value."); + maxLinkRate = 0x14; + break; + } + } else { + DBGLOG("igfx", "MLR: No custom max link rate specified. Will use 0x14 as the default value."); + } + // Enable CFL backlight patch on mobile CFL or if IGPU propery enable-cfl-backlight-fix is set int bkl = 0; if (PE_parse_boot_argn("igfxcflbklt", &bkl, sizeof(bkl))) @@ -314,6 +343,25 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } } + if (maxLinkRatePatch) { + auto symbolAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + + if (symbolAddress) { + patcher.eraseCoverageInstPrefix(symbolAddress); + + auto orgImp = reinterpret_cast(patcher.routeFunction(symbolAddress, reinterpret_cast(wrapReadAUX), true)); + + if (orgImp) { + orgReadAUX = orgImp; + SYSLOG("igfx", "MLR: ReadAUX() has been routed successfully."); + } else { + SYSLOG("igfx", "MLR: Failed to route ReadAUX()."); + } + } else { + SYSLOG("igfx", "MLR: Failed to find ReadAUX()."); + } + } + if (blackScreenPatch) { bool foundSymbol = false; @@ -525,6 +573,58 @@ bool IGFX::wrapAcceleratorStart(IOService *that, IOService *provider) { return FunctionCast(wrapAcceleratorStart, callbackIGFX->orgAcceleratorStart)(that, provider); } +/** + * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer + */ +int IGFX::wrapReadAUX(void* that, void* framebuffer, uint32_t address, uint16_t length, void* buffer, void* displayPath) { + + // + // Abstract: + // + // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of + // an invalid value of maximum link rate reported by DPCD of the builtin display. + // + // One of those fields is the number of lanes is later used as a divisor during + // the link training, resulting in a kernel panic triggered by a divison-by-zero. + // + // DPCD are retrieved from the display via a helper function named ReadAUX(). + // This wrapper function checks whether the driver is reading receiver capabilities + // from DPCD of the builtin display and then provides a custom maximum link rate value, + // so that we don't need to update the binary patch on each system update. + // + // If you are interested in the story behind this fix, take a look at my blog posts. + // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + + // Call the original ReadAUX() function to read from DPCD + int retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); + + // Guard: Check the DPCD register address + // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) + if (address != 0) + return retVal; + + // The driver tries to read the first 16 bytes from DPCD + // Get the current framebuffer index (field at 0x1dc in a framebuffer instance) + uint32_t port = *reinterpret_cast(reinterpret_cast(framebuffer) + 0x1dc); + + // Guard: Check the framebuffer port index + // By default, FB 0 refers the builtin display + if (port != 0) + // The driver is reading DPCD for an external display + return retVal; + + // The driver tries to read the receiver capabilities for the builtin display + struct DPCDCap16* caps = reinterpret_cast(buffer); + + // Set the custom maximum link rate value + caps->maxLinkRate = callbackIGFX->maxLinkRate; + + SYSLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); + + return retVal; +} + void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { if (reg == BXT_BLC_PWM_FREQ1) { if (value && value != callbackIGFX->driverBacklightFrequency) { diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 33bdb828..132ac6cf 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -246,6 +246,11 @@ class IGFX { */ void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; + + /** + * Original AppleIntelFramebufferController::ReadAUX function + */ + int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; /** * Detected CPU generation of the host system @@ -274,7 +279,12 @@ class IGFX { */ CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; - /** + /** + * Patch the maximum link rate in the DPCD buffer read from the built-in display + */ + bool maxLinkRatePatch {false}; + + /** * Set to true if PAVP code should be disabled */ bool pavpDisablePatch {false}; @@ -394,6 +404,60 @@ class IGFX { * Driver-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 write attempt at system start. */ uint32_t driverBacklightFrequency {}; + + /** + * Represents the first 16 fields of the receiver capabilities defined in DPCD + * + * Main Reference: + * - DisplayPort Specification Version 1.2 + * + * Side Reference: + * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) + * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h + */ + struct DPCDCap16 // 16 bytes + { + // DPCD Revision (DP Config Version) + // Value: 0x10, 0x11, 0x12, 0x13, 0x14 + uint8_t revision; + + // Maximum Link Rate + // Value: 0x1E (HBR3) 8.1 Gbps + // 0x14 (HBR2) 5.4 Gbps + // 0x0C (3_24) 3.24 Gbps + // 0x0A (HBR) 2.7 Gbps + // 0x06 (RBR) 1.62 Gbps + // Reference: 0x0C is used by Apple internally. + uint8_t maxLinkRate; + + // Maximum Number of Lanes + // Value: 0x1 (HBR2) + // 0x2 (HBR) + // 0x4 (RBR) + // Side Notes: + // (1) Bit 7 is used to indicate whether the link is capable of enhanced framing. + // (2) Bit 6 is used to indicate whether TPS3 is supported. + uint8_t maxLaneCount; + + // Maximum Downspread + uint8_t maxDownspread; + + // Other fields omitted in this struct + // Detailed information can be found in the specification + uint8_t others[12]; + }; + + /** + * User-specified maximum link rate value in the DPCD buffer + * + * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display + */ + uint8_t maxLinkRate {0x14}; + + /** + * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer + */ + static int wrapReadAUX(void* that, void* framebuffer, uint32_t address, uint16_t length, void* buffer, void* displayPath); /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates From 7577d21bdb11ad6e839e945a82ff55b80c76b456 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 06:21:32 -0700 Subject: [PATCH 002/102] Added the manual for the DPCD max link rate fix. --- Manual/FAQ.IntelHD.cn.md | 8 ++++++++ Manual/FAQ.IntelHD.en.md | 12 ++++++++++-- Manual/Img/dpcd_mlr.png | Bin 0 -> 91746 bytes README.md | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 Manual/Img/dpcd_mlr.png diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index b9cf460f..b34dd186 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1687,6 +1687,14 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dGPU_off.png) +## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) +为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 +![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) +另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 +4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 +可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 +若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 + ## 已知问题 *兼容性*: - 受限制的显卡:HD2000 和 HD2500,它们只能用于 IQSV (因为在白苹果中它们只用来干这个),无解。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 41076d06..6763059a 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1614,8 +1614,16 @@ Or instead of this property use the boot-arg `-cdfon` ## Disabling a discrete graphics card Add the `disable-external-gpu` property to `IGPU`. ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dGPU_off.png) -Or instead of this property, use the boot-arg `-wegnoegpu` - +Or instead of this property, use the boot-arg `-wegnoegpu` + +## Fix the invalid maximum link rate issue on some laptops (Dell XPS 15 9570, etc.) +Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. +Or instead of this property, use the boot-arg `-igfxmlr`. +![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) +You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. +Typically use `0x14` for 4K display and `0x0A` for 1080p display. +All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2), `0x1E` (HBR3). +If an invalid value is specified, the default value `0x14` will be used instead. ## Known Issues *Compatibility*: diff --git a/Manual/Img/dpcd_mlr.png b/Manual/Img/dpcd_mlr.png new file mode 100644 index 0000000000000000000000000000000000000000..2e27c42a187d69d6cc0ad1e41bf6cfa8cfc5c7e8 GIT binary patch literal 91746 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 a-{4w5_B8Yw_q4wU{Ny1|rAwp?ef|SJFv@KJ literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 40e5ffd0..c2f41e44 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,13 @@ Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) an - `-igfxfbdump` to dump native and patched framebuffer table to ioreg at IOService:/IOResources/WhateverGreen - `igfxcflbklt=1` boot argument (and `enable-cfl-backlight-fix` property) to enable CFL backlight patch - `applbkl=0` boot argument to disable AppleBacklight.kext patches for IGPU. In case of custom AppleBacklight profile- [read here.](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.OldPlugins.en.md) +- `-igfxmlr` boot argument (and `enable-dpcd-max-link-rate-fix` property) to apply the maximum link rate fix. #### Credits - [Apple](https://www.apple.com) for macOS - [AMD](https://www.amd.com) for ATOM VBIOS parsing code - [The PCI ID Repository](http://pci-ids.ucw.cz) for multiple GPU model names +- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix - [Floris497](https://github.com/Floris497) for the CoreDisplay [patches](https://github.com/Floris497/mac-pixel-clock-patch-v2) - [Fraxul](https://github.com/Fraxul) for original CFL backlight patch - [headkaze](https://github.com/headkaze) for Intel framebuffer patching code and CFL backlight patch improvements From 156af68fb0f50eeacb562c6cc492b890cdc26b48 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 06:46:53 -0700 Subject: [PATCH 003/102] Fix the formatting issue in the manual. --- Manual/FAQ.IntelHD.cn.md | 10 +++++----- Manual/FAQ.IntelHD.en.md | 15 ++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index b34dd186..8b59abad 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1688,12 +1688,12 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dGPU_off.png) ## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) -为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 +为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) -另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 -4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 -可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 -若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 +另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 +4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 +可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 +若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 ## 已知问题 *兼容性*: diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 6763059a..cd39104b 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1617,14 +1617,15 @@ Add the `disable-external-gpu` property to `IGPU`. Or instead of this property, use the boot-arg `-wegnoegpu` ## Fix the invalid maximum link rate issue on some laptops (Dell XPS 15 9570, etc.) -Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. -Or instead of this property, use the boot-arg `-igfxmlr`. +Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. +Or instead of this property, use the boot-arg `-igfxmlr`. ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) -You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. -Typically use `0x14` for 4K display and `0x0A` for 1080p display. -All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2), `0x1E` (HBR3). -If an invalid value is specified, the default value `0x14` will be used instead. - +You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. +Typically use `0x14` for 4K display and `0x0A` for 1080p display. +All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). +If an invalid value is specified, the default value `0x14` will be used instead. +If this property is not specified, same as above. + ## Known Issues *Compatibility*: - Limited cards: HD2000, HD2500 can only be used for [IQSV](https://www.applelife.ru/threads/zavod-intel-quick-sync-video.817923/) (they are used in real Macs only for this), there are no solutions. From 6a572ec8992ca304df4cc7dbc7c44425d447c77b Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 06:52:13 -0700 Subject: [PATCH 004/102] Adjust the alignment of the screen capture in the manual. --- Manual/FAQ.IntelHD.cn.md | 2 +- Manual/FAQ.IntelHD.en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 8b59abad..1728fed9 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1689,7 +1689,7 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) 为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 -![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) +![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) 另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index cd39104b..a9afd636 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1619,7 +1619,7 @@ Or instead of this property, use the boot-arg `-wegnoegpu` ## Fix the invalid maximum link rate issue on some laptops (Dell XPS 15 9570, etc.) Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. Or instead of this property, use the boot-arg `-igfxmlr`. -![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) +![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). From 6421269e461d79968d24b913661b17c1c89aac82 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 09:20:41 -0700 Subject: [PATCH 005/102] Adjust the code style. --- WhateverGreen/kern_igfx.cpp | 31 ++++++++++++-------------- WhateverGreen/kern_igfx.hpp | 44 ++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 8a47ca78..4a3a0d01 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -228,7 +228,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { hdmiAutopatch = !applyFramebufferPatch && !connectorLessFrame && getKernelVersion() >= Yosemite && !checkKernelArgument("-igfxnohdmi"); // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off; + switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch; switchOffGraphics = !pavpDisablePatch && !forceOpenGL && !moderniseAccelerator && !avoidFirmwareLoading && !readDescriptorPatch; } else { switchOffGraphics = switchOffFramebuffer = true; @@ -344,17 +344,14 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } if (maxLinkRatePatch) { - auto symbolAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); - - if (symbolAddress) { - patcher.eraseCoverageInstPrefix(symbolAddress); - - auto orgImp = reinterpret_cast(patcher.routeFunction(symbolAddress, reinterpret_cast(wrapReadAUX), true)); - - if (orgImp) { - orgReadAUX = orgImp; - SYSLOG("igfx", "MLR: ReadAUX() has been routed successfully."); + auto readAUXAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + if (readAUXAddress) { + patcher.eraseCoverageInstPrefix(readAUXAddress); + orgReadAUX = reinterpret_cast(patcher.routeFunction(readAUXAddress, reinterpret_cast(wrapReadAUX), true)); + if (orgReadAUX) { + DBGLOG("igfx", "MLR: ReadAUX() has been routed successfully."); } else { + patcher.clearError(); SYSLOG("igfx", "MLR: Failed to route ReadAUX()."); } } else { @@ -576,7 +573,7 @@ bool IGFX::wrapAcceleratorStart(IOService *that, IOService *provider) { /** * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer */ -int IGFX::wrapReadAUX(void* that, void* framebuffer, uint32_t address, uint16_t length, void* buffer, void* displayPath) { +int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { // // Abstract: @@ -606,21 +603,21 @@ int IGFX::wrapReadAUX(void* that, void* framebuffer, uint32_t address, uint16_t // The driver tries to read the first 16 bytes from DPCD // Get the current framebuffer index (field at 0x1dc in a framebuffer instance) - uint32_t port = *reinterpret_cast(reinterpret_cast(framebuffer) + 0x1dc); + auto index = getMember(framebuffer, 0x1dc); - // Guard: Check the framebuffer port index + // Guard: Check the framebuffer index // By default, FB 0 refers the builtin display - if (port != 0) + if (index != 0) // The driver is reading DPCD for an external display return retVal; // The driver tries to read the receiver capabilities for the builtin display - struct DPCDCap16* caps = reinterpret_cast(buffer); + auto caps = reinterpret_cast(buffer); // Set the custom maximum link rate value caps->maxLinkRate = callbackIGFX->maxLinkRate; - SYSLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); + DBGLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); return retVal; } diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 132ac6cf..9d8120c0 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -246,11 +246,11 @@ class IGFX { */ void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; - - /** - * Original AppleIntelFramebufferController::ReadAUX function - */ - int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; + + /** + * Original AppleIntelFramebufferController::ReadAUX function + */ + int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; /** * Detected CPU generation of the host system @@ -279,12 +279,12 @@ class IGFX { */ CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; - /** - * Patch the maximum link rate in the DPCD buffer read from the built-in display - */ - bool maxLinkRatePatch {false}; - - /** + /** + * Patch the maximum link rate in the DPCD buffer read from the built-in display + */ + bool maxLinkRatePatch {false}; + + /** * Set to true if PAVP code should be disabled */ bool pavpDisablePatch {false}; @@ -447,17 +447,17 @@ class IGFX { uint8_t others[12]; }; - /** - * User-specified maximum link rate value in the DPCD buffer - * - * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display - */ - uint8_t maxLinkRate {0x14}; - - /** - * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer - */ - static int wrapReadAUX(void* that, void* framebuffer, uint32_t address, uint16_t length, void* buffer, void* displayPath); + /** + * User-specified maximum link rate value in the DPCD buffer + * + * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display + */ + uint8_t maxLinkRate {0x14}; + + /** + * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer + */ + static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates From 7013d2245791c1da025c512b98288c93e235423e Mon Sep 17 00:00:00 2001 From: FireWolf Date: Thu, 28 Mar 2019 20:24:03 -0700 Subject: [PATCH 006/102] A better approach to retrieve the current framebuffer index. --- WhateverGreen/kern_igfx.cpp | 15 +++++++++++---- WhateverGreen/kern_igfx.hpp | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 4a3a0d01..3bc75fce 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -581,7 +581,7 @@ int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of // an invalid value of maximum link rate reported by DPCD of the builtin display. // - // One of those fields is the number of lanes is later used as a divisor during + // One of those fields, namely the number of lanes, is later used as a divisor during // the link training, resulting in a kernel panic triggered by a divison-by-zero. // // DPCD are retrieved from the display via a helper function named ReadAUX(). @@ -602,12 +602,19 @@ int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address return retVal; // The driver tries to read the first 16 bytes from DPCD - // Get the current framebuffer index (field at 0x1dc in a framebuffer instance) - auto index = getMember(framebuffer, 0x1dc); + // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) + // We read the value of "IOFBDependentIndex" instead of accessing that field directly + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + + // Guard: Should be able to retrieve the index from the registry + if (!index) { + SYSLOG("igfx", "MLR: wrapReadAUX: Failed to read the current framebuffer index."); + return retVal; + } // Guard: Check the framebuffer index // By default, FB 0 refers the builtin display - if (index != 0) + if (index->unsigned32BitValue() != 0) // The driver is reading DPCD for an external display return retVal; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 9d8120c0..fd526dbf 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -455,7 +455,7 @@ class IGFX { uint8_t maxLinkRate {0x14}; /** - * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer + * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer */ static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); From 07c35d8fd51549484469585b86372db241b90265 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 9 Apr 2019 20:17:45 -0700 Subject: [PATCH 007/102] [SYNC] Replace the DPCD image in the manual. --- Manual/Img/dpcd_mlr.png | Bin 91746 -> 28498 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Manual/Img/dpcd_mlr.png b/Manual/Img/dpcd_mlr.png index 2e27c42a187d69d6cc0ad1e41bf6cfa8cfc5c7e8..0838e76ac1f6e56553ecdf0ac802f0fb663b792e 100644 GIT binary patch literal 28498 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%0AQA-hL*FIlA@ro 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 a-{4w5_B8Yw_q4wU{Ny1|rAwp?ef|SJFv@KJ From b484c7bb181f14c2605b0927ad11f4ede1bced97 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 13:30:34 +0800 Subject: [PATCH 008/102] Fix the indentation in kern_igfx. --- WhateverGreen/kern_igfx.cpp | 2445 ++++++++++++++++++----------------- WhateverGreen/kern_igfx.hpp | 1236 +++++++++--------- 2 files changed, 1852 insertions(+), 1829 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index d27b1a01..07d7d70c 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -55,89 +55,89 @@ static KernelPatcher::KextInfo kextIntelICLHPFb { "com.apple.driver.AppleIntelIC IGFX *IGFX::callbackIGFX; void IGFX::init() { - callbackIGFX = this; - - int canLoadGuC = 0; - if (getKernelVersion() >= KernelVersion::HighSierra) - PE_parse_boot_argn("igfxfw", &canLoadGuC, sizeof(canLoadGuC)); - - uint32_t family = 0, model = 0; - cpuGeneration = CPUInfo::getGeneration(&family, &model); - switch (cpuGeneration) { - case CPUInfo::CpuGeneration::Penryn: - case CPUInfo::CpuGeneration::Nehalem: - case CPUInfo::CpuGeneration::Westmere: - // Do not warn about legacy processors (e.g. Xeon). - break; - case CPUInfo::CpuGeneration::SandyBridge: { - int tmp = 1; - PE_parse_boot_argn("igfxsnb", &tmp, sizeof(tmp)); - moderniseAccelerator = tmp == 1; - currentGraphics = &kextIntelHD3000; - currentFramebuffer = &kextIntelSNBFb; - break; - } - case CPUInfo::CpuGeneration::IvyBridge: - currentGraphics = &kextIntelHD4000; - currentFramebuffer = &kextIntelCapriFb; - break; - case CPUInfo::CpuGeneration::Haswell: - currentGraphics = &kextIntelHD5000; - currentFramebuffer = &kextIntelAzulFb; - break; - case CPUInfo::CpuGeneration::Broadwell: - currentGraphics = &kextIntelBDW; - currentFramebuffer = &kextIntelBDWFb; - break; - case CPUInfo::CpuGeneration::Skylake: - avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; - loadGuCFirmware = canLoadGuC > 0; - currentGraphics = &kextIntelSKL; - currentFramebuffer = &kextIntelSKLFb; - break; - case CPUInfo::CpuGeneration::KabyLake: - avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; - loadGuCFirmware = canLoadGuC > 0; - currentGraphics = &kextIntelKBL; - currentFramebuffer = &kextIntelKBLFb; - break; - case CPUInfo::CpuGeneration::CoffeeLake: - avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; - loadGuCFirmware = canLoadGuC > 0; - currentGraphics = &kextIntelKBL; - currentFramebuffer = &kextIntelCFLFb; - // Allow faking ask KBL - currentFramebufferOpt = &kextIntelKBLFb; - // Note, several CFL GPUs are completely broken. They freeze in IGMemoryManager::initCache due to incompatible - // configuration, supposedly due to Apple not supporting new MOCS table and forcing Skylake-based format. - // See: https://github.com/torvalds/linux/blob/135c5504a600ff9b06e321694fbcac78a9530cd4/drivers/gpu/drm/i915/intel_mocs.c#L181 - break; - case CPUInfo::CpuGeneration::CannonLake: - avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; - loadGuCFirmware = canLoadGuC > 0; - currentGraphics = &kextIntelCNL; - currentFramebuffer = &kextIntelCNLFb; - break; - case CPUInfo::CpuGeneration::IceLake: - avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; - loadGuCFirmware = canLoadGuC > 0; - currentGraphics = &kextIntelICL; - currentFramebuffer = &kextIntelICLLPFb; - currentFramebufferOpt = &kextIntelICLHPFb; - break; - default: - SYSLOG("igfx", "found an unsupported processor 0x%X:0x%X, please report this!", family, model); - break; - } - - if (currentGraphics) - lilu.onKextLoadForce(currentGraphics); - - if (currentFramebuffer) - lilu.onKextLoadForce(currentFramebuffer); - - if (currentFramebufferOpt) - lilu.onKextLoadForce(currentFramebufferOpt); + callbackIGFX = this; + + int canLoadGuC = 0; + if (getKernelVersion() >= KernelVersion::HighSierra && getKernelVersion() <= KernelVersion::Mojave) + PE_parse_boot_argn("igfxfw", &canLoadGuC, sizeof(canLoadGuC)); + + uint32_t family = 0, model = 0; + cpuGeneration = CPUInfo::getGeneration(&family, &model); + switch (cpuGeneration) { + case CPUInfo::CpuGeneration::Penryn: + case CPUInfo::CpuGeneration::Nehalem: + case CPUInfo::CpuGeneration::Westmere: + // Do not warn about legacy processors (e.g. Xeon). + break; + case CPUInfo::CpuGeneration::SandyBridge: { + int tmp = 1; + PE_parse_boot_argn("igfxsnb", &tmp, sizeof(tmp)); + moderniseAccelerator = tmp == 1; + currentGraphics = &kextIntelHD3000; + currentFramebuffer = &kextIntelSNBFb; + break; + } + case CPUInfo::CpuGeneration::IvyBridge: + currentGraphics = &kextIntelHD4000; + currentFramebuffer = &kextIntelCapriFb; + break; + case CPUInfo::CpuGeneration::Haswell: + currentGraphics = &kextIntelHD5000; + currentFramebuffer = &kextIntelAzulFb; + break; + case CPUInfo::CpuGeneration::Broadwell: + currentGraphics = &kextIntelBDW; + currentFramebuffer = &kextIntelBDWFb; + break; + case CPUInfo::CpuGeneration::Skylake: + avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; + loadGuCFirmware = canLoadGuC > 0; + currentGraphics = &kextIntelSKL; + currentFramebuffer = &kextIntelSKLFb; + break; + case CPUInfo::CpuGeneration::KabyLake: + avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; + loadGuCFirmware = canLoadGuC > 0; + currentGraphics = &kextIntelKBL; + currentFramebuffer = &kextIntelKBLFb; + break; + case CPUInfo::CpuGeneration::CoffeeLake: + avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; + loadGuCFirmware = canLoadGuC > 0; + currentGraphics = &kextIntelKBL; + currentFramebuffer = &kextIntelCFLFb; + // Allow faking ask KBL + currentFramebufferOpt = &kextIntelKBLFb; + // Note, several CFL GPUs are completely broken. They freeze in IGMemoryManager::initCache due to incompatible + // configuration, supposedly due to Apple not supporting new MOCS table and forcing Skylake-based format. + // See: https://github.com/torvalds/linux/blob/135c5504a600ff9b06e321694fbcac78a9530cd4/drivers/gpu/drm/i915/intel_mocs.c#L181 + break; + case CPUInfo::CpuGeneration::CannonLake: + avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; + loadGuCFirmware = canLoadGuC > 0; + currentGraphics = &kextIntelCNL; + currentFramebuffer = &kextIntelCNLFb; + break; + case CPUInfo::CpuGeneration::IceLake: + avoidFirmwareLoading = getKernelVersion() >= KernelVersion::HighSierra; + loadGuCFirmware = canLoadGuC > 0; + currentGraphics = &kextIntelICL; + currentFramebuffer = &kextIntelICLLPFb; + currentFramebufferOpt = &kextIntelICLHPFb; + break; + default: + SYSLOG("igfx", "found an unsupported processor 0x%X:0x%X, please report this!", family, model); + break; + } + + if (currentGraphics) + lilu.onKextLoadForce(currentGraphics); + + if (currentFramebuffer) + lilu.onKextLoadForce(currentFramebuffer); + + if (currentFramebufferOpt) + lilu.onKextLoadForce(currentFramebufferOpt); } void IGFX::deinit() { @@ -145,1060 +145,1083 @@ void IGFX::deinit() { } void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { - bool switchOffGraphics = false; - bool switchOffFramebuffer = false; - framebufferPatch.framebufferId = info->reportedFramebufferId; + bool switchOffGraphics = false; + bool switchOffFramebuffer = false; + framebufferPatch.framebufferId = info->reportedFramebufferId; - if (info->videoBuiltin) { - applyFramebufferPatch = loadPatchesFromDevice(info->videoBuiltin, info->reportedFramebufferId); + if (info->videoBuiltin) { + applyFramebufferPatch = loadPatchesFromDevice(info->videoBuiltin, info->reportedFramebufferId); #ifdef DEBUG - if (checkKernelArgument("-igfxdump")) - dumpFramebufferToDisk = true; + if (checkKernelArgument("-igfxdump")) + dumpFramebufferToDisk = true; - if (checkKernelArgument("-igfxfbdump")) - dumpPlatformTable = true; + if (checkKernelArgument("-igfxfbdump")) + dumpPlatformTable = true; #endif - - // Enable maximum link rate patch if the corresponding boot argument is found + + // Enable maximum link rate patch if the corresponding boot argument is found maxLinkRatePatch = checkKernelArgument("-igfxmlr"); // Or if "enable-dpcd-max-link-rate-fix" is set in IGPU property if (!maxLinkRatePatch) maxLinkRatePatch = info->videoBuiltin->getProperty("enable-dpcd-max-link-rate-fix") != nullptr; - - // Read the custom maximum link rate if present - if (WIOKit::getOSDataValue(info->videoBuiltin, "dpcd-max-link-rate", maxLinkRate)) { - // Guard: Verify the custom link rate before using it - switch (maxLinkRate) { - case 0x1E: // HBR3 8.1 Gbps - case 0x14: // HBR2 5.4 Gbps - case 0x0C: // 3_24 3.24 Gbps Used by Apple internally - case 0x0A: // HBR 2.7 Gbps - case 0x06: // RBR 1.62 Gbps - DBGLOG("igfx", "MLR: Found a valid custom maximum link rate value 0x%02x", maxLinkRate); - break; - - default: - // Invalid link rate value - SYSLOG("igfx", "MLR: Found an invalid custom maximum link rate value. Will use 0x14 as a fallback value."); - maxLinkRate = 0x14; - break; - } - } else { - DBGLOG("igfx", "MLR: No custom max link rate specified. Will use 0x14 as the default value."); - } - - // Enable CFL backlight patch on mobile CFL or if IGPU propery enable-cfl-backlight-fix is set - int bkl = 0; - if (PE_parse_boot_argn("igfxcflbklt", &bkl, sizeof(bkl))) - cflBacklightPatch = bkl == 1 ? CoffeeBacklightPatch::On : CoffeeBacklightPatch::Off; - else if (info->videoBuiltin->getProperty("enable-cfl-backlight-fix")) - cflBacklightPatch = CoffeeBacklightPatch::On; - else if (currentFramebuffer == &kextIntelCFLFb && WIOKit::getComputerModel() == WIOKit::ComputerModel::ComputerLaptop) - cflBacklightPatch = CoffeeBacklightPatch::Auto; - - if (WIOKit::getOSDataValue(info->videoBuiltin, "max-backlight-freq", targetBacklightFrequency)) - DBGLOG("igfx", "read custom backlight frequency %u", targetBacklightFrequency); - - bool connectorLessFrame = info->reportedFramebufferIsConnectorLess; - - // Black screen (ComputeLaneCount) happened from 10.12.4 - // It only affects SKL, KBL, and CFL drivers with a frame with connectors. - if (!connectorLessFrame && cpuGeneration >= CPUInfo::CpuGeneration::Skylake && - ((getKernelVersion() == KernelVersion::Sierra && getKernelMinorVersion() >= 5) || getKernelVersion() >= KernelVersion::HighSierra)) { - blackScreenPatch = info->firmwareVendor != DeviceInfo::FirmwareVendor::Apple; - } - - // GuC firmware is just fine on Apple hardware - if (info->firmwareVendor == DeviceInfo::FirmwareVendor::Apple) - avoidFirmwareLoading = false; - - // PAVP patch is only necessary when we have no discrete GPU - pavpDisablePatch = !connectorLessFrame && info->firmwareVendor != DeviceInfo::FirmwareVendor::Apple; - - int gl = info->videoBuiltin->getProperty("disable-metal") != nullptr; - PE_parse_boot_argn("igfxgl", &gl, sizeof(gl)); - forceOpenGL = gl == 1; - - // Starting from 10.14.4b1 KabyLake graphics randomly kernel panics on GPU usage - readDescriptorPatch = cpuGeneration == CPUInfo::CpuGeneration::KabyLake && getKernelVersion() >= KernelVersion::Mojave; - - // Automatically enable HDMI -> DP patches - hdmiAutopatch = !applyFramebufferPatch && !connectorLessFrame && getKernelVersion() >= Yosemite && !checkKernelArgument("-igfxnohdmi"); - - // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch; - switchOffGraphics = !pavpDisablePatch && !forceOpenGL && !moderniseAccelerator && !avoidFirmwareLoading && !readDescriptorPatch; - } else { - switchOffGraphics = switchOffFramebuffer = true; - } - - if (switchOffGraphics && currentGraphics) - currentGraphics->switchOff(); - - if (switchOffFramebuffer) { - if (currentFramebuffer) - currentFramebuffer->switchOff(); - if (currentFramebufferOpt) - currentFramebufferOpt->switchOff(); - } - - if (moderniseAccelerator) { - KernelPatcher::RouteRequest request("__ZN9IOService20copyExistingServicesEP12OSDictionaryjj", wrapCopyExistingServices, orgCopyExistingServices); - patcher.routeMultiple(KernelPatcher::KernelID, &request, 1); - } + + // Read the custom maximum link rate if present + if (WIOKit::getOSDataValue(info->videoBuiltin, "dpcd-max-link-rate", maxLinkRate)) { + // Guard: Verify the custom link rate before using it + switch (maxLinkRate) { + case 0x1E: // HBR3 8.1 Gbps + case 0x14: // HBR2 5.4 Gbps + case 0x0C: // 3_24 3.24 Gbps Used by Apple internally + case 0x0A: // HBR 2.7 Gbps + case 0x06: // RBR 1.62 Gbps + DBGLOG("igfx", "MLR: Found a valid custom maximum link rate value 0x%02x", maxLinkRate); + break; + + default: + // Invalid link rate value + SYSLOG("igfx", "MLR: Found an invalid custom maximum link rate value. Will use 0x14 as a fallback value."); + maxLinkRate = 0x14; + break; + } + } else { + DBGLOG("igfx", "MLR: No custom max link rate specified. Will use 0x14 as the default value."); + } + + // Enable CFL backlight patch on mobile CFL or if IGPU propery enable-cfl-backlight-fix is set + int bkl = 0; + if (PE_parse_boot_argn("igfxcflbklt", &bkl, sizeof(bkl))) + cflBacklightPatch = bkl == 1 ? CoffeeBacklightPatch::On : CoffeeBacklightPatch::Off; + else if (info->videoBuiltin->getProperty("enable-cfl-backlight-fix")) + cflBacklightPatch = CoffeeBacklightPatch::On; + else if (currentFramebuffer == &kextIntelCFLFb && WIOKit::getComputerModel() == WIOKit::ComputerModel::ComputerLaptop) + cflBacklightPatch = CoffeeBacklightPatch::Auto; + + if (WIOKit::getOSDataValue(info->videoBuiltin, "max-backlight-freq", targetBacklightFrequency)) + DBGLOG("igfx", "read custom backlight frequency %u", targetBacklightFrequency); + + bool connectorLessFrame = info->reportedFramebufferIsConnectorLess; + + // Black screen (ComputeLaneCount) happened from 10.12.4 + // It only affects SKL, KBL, and CFL drivers with a frame with connectors. + if (!connectorLessFrame && cpuGeneration >= CPUInfo::CpuGeneration::Skylake && + ((getKernelVersion() == KernelVersion::Sierra && getKernelMinorVersion() >= 5) || getKernelVersion() >= KernelVersion::HighSierra)) { + blackScreenPatch = info->firmwareVendor != DeviceInfo::FirmwareVendor::Apple; + } + + // GuC firmware is just fine on Apple hardware + if (info->firmwareVendor == DeviceInfo::FirmwareVendor::Apple) + avoidFirmwareLoading = false; + + // PAVP patch is only necessary when we have no discrete GPU + pavpDisablePatch = !connectorLessFrame && info->firmwareVendor != DeviceInfo::FirmwareVendor::Apple; + + int gl = info->videoBuiltin->getProperty("disable-metal") != nullptr; + PE_parse_boot_argn("igfxgl", &gl, sizeof(gl)); + forceOpenGL = gl == 1; + + // Starting from 10.14.4b1 KabyLake graphics randomly kernel panics on GPU usage + readDescriptorPatch = cpuGeneration >= CPUInfo::CpuGeneration::KabyLake && getKernelVersion() >= KernelVersion::Mojave; + + // Automatically enable HDMI -> DP patches + hdmiAutopatch = !applyFramebufferPatch && !connectorLessFrame && getKernelVersion() >= Yosemite && !checkKernelArgument("-igfxnohdmi"); + + // Disable kext patching if we have nothing to do. + switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch; + switchOffGraphics = !pavpDisablePatch && !forceOpenGL && !moderniseAccelerator && !avoidFirmwareLoading && !readDescriptorPatch; + } else { + switchOffGraphics = switchOffFramebuffer = true; + } + + if (switchOffGraphics && currentGraphics) + currentGraphics->switchOff(); + + if (switchOffFramebuffer) { + if (currentFramebuffer) + currentFramebuffer->switchOff(); + if (currentFramebufferOpt) + currentFramebufferOpt->switchOff(); + } + + if (moderniseAccelerator) { + KernelPatcher::RouteRequest request("__ZN9IOService20copyExistingServicesEP12OSDictionaryjj", wrapCopyExistingServices, orgCopyExistingServices); + patcher.routeMultiple(KernelPatcher::KernelID, &request, 1); + } } bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - if (currentGraphics && currentGraphics->loadIndex == index) { - if (pavpDisablePatch) { - auto callbackSym = "__ZN16IntelAccelerator19PAVPCommandCallbackE22PAVPSessionCommandID_tjPjb"; - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) - callbackSym = "__ZN15Gen6Accelerator19PAVPCommandCallbackE22PAVPSessionCommandID_t18PAVPSessionAppID_tPjb"; - else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) - callbackSym = "__ZN16IntelAccelerator19PAVPCommandCallbackE22PAVPSessionCommandID_t18PAVPSessionAppID_tPjb"; - - KernelPatcher::RouteRequest request(callbackSym, wrapPavpSessionCallback, orgPavpSessionCallback); - patcher.routeMultiple(index, &request, 1, address, size); - } - - if (forceOpenGL || moderniseAccelerator || avoidFirmwareLoading) { - auto startSym = "__ZN16IntelAccelerator5startEP9IOService"; - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) - startSym = "__ZN16IntelAccelerator5startEP9IOService"; - - KernelPatcher::RouteRequest request(startSym, wrapAcceleratorStart, orgAcceleratorStart); - patcher.routeMultiple(index, &request, 1, address, size); - - if (loadGuCFirmware) - loadIGScheduler4Patches(patcher, index, address, size); - } - - if (readDescriptorPatch) { - KernelPatcher::RouteRequest request("__ZNK25IGHardwareGlobalPageTable4readEyRyS0_", globalPageTableRead); - patcher.routeMultiple(index, &request, 1, address, size); - } - - return true; - } - - if ((currentFramebuffer && currentFramebuffer->loadIndex == index) || - (currentFramebufferOpt && currentFramebufferOpt->loadIndex == index)) { - // Find actual framebuffer used (kaby or coffee) - auto realFramebuffer = (currentFramebuffer && currentFramebuffer->loadIndex == index) ? currentFramebuffer : currentFramebufferOpt; - // Accept Coffee FB and enable backlight patches unless Off (Auto turns them on by default). - bool bklCoffeeFb = realFramebuffer == &kextIntelCFLFb && cflBacklightPatch != CoffeeBacklightPatch::Off; - // Accept Kaby FB and enable backlight patches if On (Auto is irrelevant here). - bool bklKabyFb = realFramebuffer == &kextIntelKBLFb && cflBacklightPatch == CoffeeBacklightPatch::On; - if (bklCoffeeFb || bklKabyFb) { - // Intel backlight is modeled via pulse-width modulation (PWM). See page 144 of: - // https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf - // Singal-wise it looks as a cycle of signal levels on the timeline: - // 22111100221111002211110022111100 (4 cycles) - // 0 - no signal, 1 - no value (no pulse), 2 - pulse (light on) - // - Physical Cycle (0+1+2) defines maximum backlight frequency, limited by HW precision. - // - Base Cycle (1+2) defines [1/PWM Base Frequency], limited by physical cycle, see BXT_BLC_PWM_FREQ1. - // - Duty Cycle (2) defines [1/PWM Increment] - backlight level, - // [PWM Frequency Divider] - backlight max, see BXT_BLC_PWM_DUTY1. - // - Duty Cycle position (first vs last) is [PWM Polarity] - // - // Duty cycle = PWM Base Frequeny * (1 / PWM Increment) / PWM Frequency Divider - // - // On macOS there are extra limitations: - // - All values and operations are u32 (32-bit unsigned) - // - [1/PWM Increment] has 0 to 0xFFFF range - // - [PWM Frequency Divider] is fixed to be 0xFFFF - // - [PWM Base Frequency] is capped by 0xFFFF (to avoid u32 wraparound), and is hardcoded - // either in Framebuffer data (pre-CFL) or in the code (CFL: 7777 or 22222). - // - // On CFL the following patches have to be applied: - // - Hardcoded [PWM Base Frequency] should be patched or set after the hardcoded value is written by patching - // hardcoded frequencies. 65535 is used by default. - // - If [PWM Base Frequency] is > 65535, to avoid a wraparound code calculating BXT_BLC_PWM_DUTY1 - // should be replaced to use 64-bit arithmetics. - // [PWM Base Frequency] can be specified via igfxbklt=1 boot-arg or backlight-base-frequency property. - - // This patch will overwrite WriteRegister32 function to rescale all the register writes of backlight controller. - // Slightly different methods are used for CFL hardware running on KBL and CFL drivers. - - auto regRead = patcher.solveSymbol - (index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); - auto regWrite = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteRegister32Emj", address, size); - if (regWrite) { - (bklCoffeeFb ? orgCflReadRegister32 : orgKblReadRegister32) = regRead; - - patcher.eraseCoverageInstPrefix(regWrite); - auto orgRegWrite = reinterpret_cast - (patcher.routeFunction(regWrite, reinterpret_cast(bklCoffeeFb ? wrapCflWriteRegister32 : wrapKblWriteRegister32), true)); - - if (orgRegWrite) { - (bklCoffeeFb ? orgCflWriteRegister32 : orgKblWriteRegister32) = orgRegWrite; - } else { - SYSLOG("igfx", "failed to route WriteRegister32 for cfl %d", bklCoffeeFb); - patcher.clearError(); - } - } else { - SYSLOG("igfx", "failed to find ReadRegister32 for cfl %d", bklCoffeeFb); - } - } - - if (maxLinkRatePatch) { - auto readAUXAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); - if (readAUXAddress) { - patcher.eraseCoverageInstPrefix(readAUXAddress); - orgReadAUX = reinterpret_cast(patcher.routeFunction(readAUXAddress, reinterpret_cast(wrapReadAUX), true)); - if (orgReadAUX) { - DBGLOG("igfx", "MLR: ReadAUX() has been routed successfully."); - } else { - patcher.clearError(); - SYSLOG("igfx", "MLR: Failed to route ReadAUX()."); - } - } else { - SYSLOG("igfx", "MLR: Failed to find ReadAUX()."); - } - } - - if (blackScreenPatch) { - bool foundSymbol = false; - - // Currently it is 10.14.1 and Kaby+... - if (getKernelVersion() >= KernelVersion::Mojave && cpuGeneration >= CPUInfo::CpuGeneration::KabyLake) { - KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController16ComputeLaneCountEPK29IODetailedTimingInformationV2jPj", wrapComputeLaneCountNouveau, orgComputeLaneCount); - foundSymbol = patcher.routeMultiple(index, &request, 1, address, size); - } - - if (!foundSymbol) { - KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController16ComputeLaneCountEPK29IODetailedTimingInformationV2jjPj", wrapComputeLaneCount, orgComputeLaneCount); - patcher.routeMultiple(index, &request, 1, address, size); - } - } - - if (applyFramebufferPatch || dumpFramebufferToDisk || dumpPlatformTable || hdmiAutopatch) { - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) { - gPlatformListIsSNB = true; - gPlatformInformationList = patcher.solveSymbol(index, "_PlatformInformationList", address, size); - } else { - gPlatformListIsSNB = false; - gPlatformInformationList = patcher.solveSymbol(index, "_gPlatformInformationList", address, size); - } - - DBGLOG("igfx", "platform is snb %d and list " PRIKADDR, gPlatformListIsSNB, CASTKADDR(gPlatformInformationList)); - - if (gPlatformInformationList) { - framebufferStart = reinterpret_cast(address); - framebufferSize = size; - - auto fbGetOSInformation = "__ZN31AppleIntelFramebufferController16getOSInformationEv"; - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) - fbGetOSInformation = "__ZN23AppleIntelSNBGraphicsFB16getOSInformationEv"; - else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) - fbGetOSInformation = "__ZN25AppleIntelCapriController16getOSInformationEv"; - else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) - fbGetOSInformation = "__ZN24AppleIntelAzulController16getOSInformationEv"; - else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) - fbGetOSInformation = "__ZN22AppleIntelFBController16getOSInformationEv"; - - KernelPatcher::RouteRequest request(fbGetOSInformation, wrapGetOSInformation, orgGetOSInformation); - patcher.routeMultiple(index, &request, 1, address, size); - } else { - SYSLOG("igfx", "failed to obtain gPlatformInformationList pointer with code %d", patcher.getError()); - patcher.clearError(); - } - } - return true; - } - - return false; + if (currentGraphics && currentGraphics->loadIndex == index) { + if (pavpDisablePatch) { + auto callbackSym = "__ZN16IntelAccelerator19PAVPCommandCallbackE22PAVPSessionCommandID_tjPjb"; + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) + callbackSym = "__ZN15Gen6Accelerator19PAVPCommandCallbackE22PAVPSessionCommandID_t18PAVPSessionAppID_tPjb"; + else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) + callbackSym = "__ZN16IntelAccelerator19PAVPCommandCallbackE22PAVPSessionCommandID_t18PAVPSessionAppID_tPjb"; + + KernelPatcher::RouteRequest request(callbackSym, wrapPavpSessionCallback, orgPavpSessionCallback); + patcher.routeMultiple(index, &request, 1, address, size); + } + + if (forceOpenGL || moderniseAccelerator || (avoidFirmwareLoading && getKernelVersion() <= KernelVersion::Mojave)) { + auto startSym = "__ZN16IntelAccelerator5startEP9IOService"; + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) + startSym = "__ZN16IntelAccelerator5startEP9IOService"; + + KernelPatcher::RouteRequest request(startSym, wrapAcceleratorStart, orgAcceleratorStart); + patcher.routeMultiple(index, &request, 1, address, size); + + if (loadGuCFirmware) + loadIGScheduler4Patches(patcher, index, address, size); + } + + // On 10.15 GraphicsSchedulerSelect 2 is removed, so we disable it by manually patching default flags. + if (avoidFirmwareLoading && getKernelVersion() > KernelVersion::Mojave) { + auto sym = patcher.solveSymbol(index, "__ZN16IntelAccelerator19populateAccelConfigEP13IOAccelConfig", address, size); + for (size_t i = 0; i < 4096; i++, sym++) { + if (sym[0] == 0x00 && sym[1] == 0x00 && sym[2] == 0x18 && sym[3] == 0x00) { + DBGLOG("igfx", "found GuC accel config at " PRIKADDR, CASTKADDR(sym)); + auto status = MachInfo::setKernelWriting(true, KernelPatcher::kernelWriteLock); + if (status == KERN_SUCCESS) { + sym[2] = 0x28; + MachInfo::setKernelWriting(false, KernelPatcher::kernelWriteLock); + } else { + SYSLOG("igfx", "GuC accel config protection upgrade failure %d", status); + } + break; + } + } + } + + if (readDescriptorPatch) { + KernelPatcher::RouteRequest request("__ZNK25IGHardwareGlobalPageTable4readEyRyS0_", globalPageTableRead); + patcher.routeMultiple(index, &request, 1, address, size); + } + + return true; + } + + if ((currentFramebuffer && currentFramebuffer->loadIndex == index) || + (currentFramebufferOpt && currentFramebufferOpt->loadIndex == index)) { + // Find actual framebuffer used (kaby or coffee) + auto realFramebuffer = (currentFramebuffer && currentFramebuffer->loadIndex == index) ? currentFramebuffer : currentFramebufferOpt; + // Accept Coffee FB and enable backlight patches unless Off (Auto turns them on by default). + bool bklCoffeeFb = realFramebuffer == &kextIntelCFLFb && cflBacklightPatch != CoffeeBacklightPatch::Off; + // Accept Kaby FB and enable backlight patches if On (Auto is irrelevant here). + bool bklKabyFb = realFramebuffer == &kextIntelKBLFb && cflBacklightPatch == CoffeeBacklightPatch::On; + if (bklCoffeeFb || bklKabyFb) { + // Intel backlight is modeled via pulse-width modulation (PWM). See page 144 of: + // https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf + // Singal-wise it looks as a cycle of signal levels on the timeline: + // 22111100221111002211110022111100 (4 cycles) + // 0 - no signal, 1 - no value (no pulse), 2 - pulse (light on) + // - Physical Cycle (0+1+2) defines maximum backlight frequency, limited by HW precision. + // - Base Cycle (1+2) defines [1/PWM Base Frequency], limited by physical cycle, see BXT_BLC_PWM_FREQ1. + // - Duty Cycle (2) defines [1/PWM Increment] - backlight level, + // [PWM Frequency Divider] - backlight max, see BXT_BLC_PWM_DUTY1. + // - Duty Cycle position (first vs last) is [PWM Polarity] + // + // Duty cycle = PWM Base Frequeny * (1 / PWM Increment) / PWM Frequency Divider + // + // On macOS there are extra limitations: + // - All values and operations are u32 (32-bit unsigned) + // - [1/PWM Increment] has 0 to 0xFFFF range + // - [PWM Frequency Divider] is fixed to be 0xFFFF + // - [PWM Base Frequency] is capped by 0xFFFF (to avoid u32 wraparound), and is hardcoded + // either in Framebuffer data (pre-CFL) or in the code (CFL: 7777 or 22222). + // + // On CFL the following patches have to be applied: + // - Hardcoded [PWM Base Frequency] should be patched or set after the hardcoded value is written by patching + // hardcoded frequencies. 65535 is used by default. + // - If [PWM Base Frequency] is > 65535, to avoid a wraparound code calculating BXT_BLC_PWM_DUTY1 + // should be replaced to use 64-bit arithmetics. + // [PWM Base Frequency] can be specified via igfxbklt=1 boot-arg or backlight-base-frequency property. + + // This patch will overwrite WriteRegister32 function to rescale all the register writes of backlight controller. + // Slightly different methods are used for CFL hardware running on KBL and CFL drivers. + + auto regRead = patcher.solveSymbol + (index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); + auto regWrite = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteRegister32Emj", address, size); + if (regWrite) { + (bklCoffeeFb ? orgCflReadRegister32 : orgKblReadRegister32) = regRead; + + patcher.eraseCoverageInstPrefix(regWrite); + auto orgRegWrite = reinterpret_cast + (patcher.routeFunction(regWrite, reinterpret_cast(bklCoffeeFb ? wrapCflWriteRegister32 : wrapKblWriteRegister32), true)); + + if (orgRegWrite) { + (bklCoffeeFb ? orgCflWriteRegister32 : orgKblWriteRegister32) = orgRegWrite; + } else { + SYSLOG("igfx", "failed to route WriteRegister32 for cfl %d", bklCoffeeFb); + patcher.clearError(); + } + } else { + SYSLOG("igfx", "failed to find ReadRegister32 for cfl %d", bklCoffeeFb); + } + } + + if (maxLinkRatePatch) { + auto readAUXAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + if (readAUXAddress) { + patcher.eraseCoverageInstPrefix(readAUXAddress); + orgReadAUX = reinterpret_cast(patcher.routeFunction(readAUXAddress, reinterpret_cast(wrapReadAUX), true)); + if (orgReadAUX) { + DBGLOG("igfx", "MLR: ReadAUX() has been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "MLR: Failed to route ReadAUX()."); + } + } else { + SYSLOG("igfx", "MLR: Failed to find ReadAUX()."); + } + } + + if (blackScreenPatch) { + bool foundSymbol = false; + + // Currently it is 10.14.1 and Kaby+... + if (getKernelVersion() >= KernelVersion::Mojave && cpuGeneration >= CPUInfo::CpuGeneration::KabyLake) { + KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController16ComputeLaneCountEPK29IODetailedTimingInformationV2jPj", wrapComputeLaneCountNouveau, orgComputeLaneCount); + foundSymbol = patcher.routeMultiple(index, &request, 1, address, size); + } + + if (!foundSymbol) { + KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController16ComputeLaneCountEPK29IODetailedTimingInformationV2jjPj", wrapComputeLaneCount, orgComputeLaneCount); + patcher.routeMultiple(index, &request, 1, address, size); + } + } + + if (applyFramebufferPatch || dumpFramebufferToDisk || dumpPlatformTable || hdmiAutopatch) { + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) { + gPlatformListIsSNB = true; + gPlatformInformationList = patcher.solveSymbol(index, "_PlatformInformationList", address, size); + } else { + gPlatformListIsSNB = false; + gPlatformInformationList = patcher.solveSymbol(index, "_gPlatformInformationList", address, size); + } + + DBGLOG("igfx", "platform is snb %d and list " PRIKADDR, gPlatformListIsSNB, CASTKADDR(gPlatformInformationList)); + + if (gPlatformInformationList) { + framebufferStart = reinterpret_cast(address); + framebufferSize = size; + + auto fbGetOSInformation = "__ZN31AppleIntelFramebufferController16getOSInformationEv"; + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) + fbGetOSInformation = "__ZN23AppleIntelSNBGraphicsFB16getOSInformationEv"; + else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) + fbGetOSInformation = "__ZN25AppleIntelCapriController16getOSInformationEv"; + else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) + fbGetOSInformation = "__ZN24AppleIntelAzulController16getOSInformationEv"; + else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) + fbGetOSInformation = "__ZN22AppleIntelFBController16getOSInformationEv"; + + KernelPatcher::RouteRequest request(fbGetOSInformation, wrapGetOSInformation, orgGetOSInformation); + patcher.routeMultiple(index, &request, 1, address, size); + } else { + SYSLOG("igfx", "failed to obtain gPlatformInformationList pointer with code %d", patcher.getError()); + patcher.clearError(); + } + } + return true; + } + + return false; } IOReturn IGFX::wrapPavpSessionCallback(void *intelAccelerator, int32_t sessionCommand, uint32_t sessionAppId, uint32_t *a4, bool flag) { - //DBGLOG("igfx, "pavpCallback: cmd = %d, flag = %d, app = %u, a4 = %s", sessionCommand, flag, sessionAppId, a4 == nullptr ? "null" : "not null"); + //DBGLOG("igfx, "pavpCallback: cmd = %d, flag = %d, app = %u, a4 = %s", sessionCommand, flag, sessionAppId, a4 == nullptr ? "null" : "not null"); - if (sessionCommand == 4) { - DBGLOG("igfx", "pavpSessionCallback: enforcing error on cmd 4 (send to ring?)!"); - return kIOReturnTimeout; // or kIOReturnSuccess - } + if (sessionCommand == 4) { + DBGLOG("igfx", "pavpSessionCallback: enforcing error on cmd 4 (send to ring?)!"); + return kIOReturnTimeout; // or kIOReturnSuccess + } - return FunctionCast(wrapPavpSessionCallback, callbackIGFX->orgPavpSessionCallback)(intelAccelerator, sessionCommand, sessionAppId, a4, flag); + return FunctionCast(wrapPavpSessionCallback, callbackIGFX->orgPavpSessionCallback)(intelAccelerator, sessionCommand, sessionAppId, a4, flag); } bool IGFX::globalPageTableRead(void *hardwareGlobalPageTable, uint64_t address, uint64_t &physAddress, uint64_t &flags) { - uint64_t pageNumber = address >> PAGE_SHIFT; - uint64_t pageEntry = getMember(hardwareGlobalPageTable, 0x28)[pageNumber]; - // PTE: Page Table Entry for 4KB Page, page 82: - // https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol05-memory_views.pdf. - physAddress = pageEntry & 0x7FFFFFF000ULL; // HAW-1:12, where HAW is 39. - flags = pageEntry & PAGE_MASK; // 11:0 - // Relevant flag bits are as follows: - // 2 Ignored Ignored (h/w does not care about values behind ignored registers) - // 1 R/W: Read/Write Write permission rights. If 0, write permission not granted for requests with user-level privilege - // (and requests with supervisor-level privilege, if WPE=1 in the relevant extended-context-entry) to - // the memory region controlled by this entry. See a later section for access rights. - // GPU does not support Supervisor mode contexts. - // 0 P: Present This bit must be “1” to point to a valid Page. - // - // In 10.14.4b1 the return condition changed from (flags & 7U) != 0 to (flags & 1U) != 0. The change makes good sense to me, but - // it results in crashes on 10.14.4b1 on many ASUS Z170 and Z270 boards with connector-less IGPU framebuffer. - // The reason for that is that __ZNK31IGHardwarePerProcessPageTable6422readDescriptorForRangeERK14IGAddressRangePPN10IGPagePool14PageDescriptorE - // unconditionally returns true but actually returns NULL PageDescriptor, which subsequently results in OSAddAtomic64(&this->counter) - // __ZN31IGHardwarePerProcessPageTable6421mapDescriptorForRangeERK14IGAddressRangePN10IGPagePool14PageDescriptorE writing to invalid address. - // - // Fault CR2: 0x0000000000000028, Error code: 0x0000000000000002, Fault CPU: 0x1, PL: 0, VF: 0 - // 0xffffff821885b8f0 : 0xffffff801d3c7dc4 mach_kernel : _OSAddAtomic64 + 0x4 - // 0xffffff821885b9e0 : 0xffffff7f9f3f1845 com.apple.driver.AppleIntelKBLGraphics : - // __ZN31IGHardwarePerProcessPageTable6421mapDescriptorForRangeERK14IGAddressRangePN10IGPagePool14PageDescriptorE + 0x6b - // 0xffffff821885ba20 : 0xffffff7f9f40a3c3 com.apple.driver.AppleIntelKBLGraphics : - // __ZN29IGHardwarePerProcessPageTable25synchronizePageDescriptorEPKS_RK14IGAddressRangeb + 0x51 - // 0xffffff821885ba50 : 0xffffff7f9f40a36b com.apple.driver.AppleIntelKBLGraphics : - // __ZN29IGHardwarePerProcessPageTable15synchronizeWithIS_EEvPKT_RK14IGAddressRangeb + 0x37 - // 0xffffff821885ba70 : 0xffffff7f9f3b1716 com.apple.driver.AppleIntelKBLGraphics : __ZN15IGMemoryManager19newPageTableForTaskEP11IGAccelTask + 0x98 - // 0xffffff821885baa0 : 0xffffff7f9f40a716 com.apple.driver.AppleIntelKBLGraphics : __ZN11IGAccelTask15initWithOptionsEP16IntelAccelerator + 0x108 - // 0xffffff821885bad0 : 0xffffff7f9f40a5f7 com.apple.driver.AppleIntelKBLGraphics : __ZN11IGAccelTask11withOptionsEP16IntelAccelerator + 0x31 - // 0xffffff821885baf0 : 0xffffff7f9f3bfaf0 com.apple.driver.AppleIntelKBLGraphics : __ZN16IntelAccelerator17createUserGPUTaskEv + 0x30 - // 0xffffff821885bb10 : 0xffffff7f9f2f7520 com.apple.iokit.IOAcceleratorFamily2 : __ZN14IOAccelShared24initEP22IOGraphicsAccelerator2P4task + 0x2e - // 0xffffff821885bb50 : 0xffffff7f9f30c157 com.apple.iokit.IOAcceleratorFamily2 : __ZN22IOGraphicsAccelerator212createSharedEP4task + 0x33 - // 0xffffff821885bb80 : 0xffffff7f9f2faa95 com.apple.iokit.IOAcceleratorFamily2 : __ZN24IOAccelSharedUserClient211sharedStartEv + 0x2b - // 0xffffff821885bba0 : 0xffffff7f9f401ca2 com.apple.driver.AppleIntelKBLGraphics : __ZN23IGAccelSharedUserClient11sharedStartEv + 0x16 - // 0xffffff821885bbc0 : 0xffffff7f9f2f8aaa com.apple.iokit.IOAcceleratorFamily2 : __ZN24IOAccelSharedUserClient25startEP9IOService + 0x9c - // 0xffffff821885bbf0 : 0xffffff7f9f30ba3c com.apple.iokit.IOAcceleratorFamily2 : __ZN22IOGraphicsAccelerator213newUserClientEP4taskPvjPP12IOUserClient + 0x43e - // 0xffffff821885bc80 : 0xffffff801d42a040 mach_kernel : __ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient + 0x30 - // 0xffffff821885bcd0 : 0xffffff801d48ef9a mach_kernel : _is_io_service_open_extended + 0x10a - // 0xffffff821885bd30 : 0xffffff801ce96b52 mach_kernel : _iokit_server_routine + 0x58d2 - // 0xffffff821885bd80 : 0xffffff801cdb506c mach_kernel : _ipc_kobject_server + 0x12c - // 0xffffff821885bdd0 : 0xffffff801cd8fa91 mach_kernel : _ipc_kmsg_send + 0xd1 - // 0xffffff821885be50 : 0xffffff801cda42fe mach_kernel : _mach_msg_overwrite_trap + 0x3ce - // 0xffffff821885bef0 : 0xffffff801cec3e87 mach_kernel : _mach_call_munger64 + 0x257 - // 0xffffff821885bfa0 : 0xffffff801cd5d2c6 mach_kernel : _hndl_mach_scall64 + 0x16 - // - // Even so the change makes good sense to me, and most likely the real bug is elsewhere. The change workarounds the issue by also checking - // for the W (writeable) bit in addition to P (present). Presumably this works because some code misuses ::read method to iterate - // over page table instead of obtaining valid mapped physical address. - return (flags & 3U) != 0; + uint64_t pageNumber = address >> PAGE_SHIFT; + uint64_t pageEntry = getMember(hardwareGlobalPageTable, 0x28)[pageNumber]; + // PTE: Page Table Entry for 4KB Page, page 82: + // https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol05-memory_views.pdf. + physAddress = pageEntry & 0x7FFFFFF000ULL; // HAW-1:12, where HAW is 39. + flags = pageEntry & PAGE_MASK; // 11:0 + // Relevant flag bits are as follows: + // 2 Ignored Ignored (h/w does not care about values behind ignored registers) + // 1 R/W: Read/Write Write permission rights. If 0, write permission not granted for requests with user-level privilege + // (and requests with supervisor-level privilege, if WPE=1 in the relevant extended-context-entry) to + // the memory region controlled by this entry. See a later section for access rights. + // GPU does not support Supervisor mode contexts. + // 0 P: Present This bit must be “1” to point to a valid Page. + // + // In 10.14.4b1 the return condition changed from (flags & 7U) != 0 to (flags & 1U) != 0. The change makes good sense to me, but + // it results in crashes on 10.14.4b1 on many ASUS Z170 and Z270 boards with connector-less IGPU framebuffer. + // The reason for that is that __ZNK31IGHardwarePerProcessPageTable6422readDescriptorForRangeERK14IGAddressRangePPN10IGPagePool14PageDescriptorE + // unconditionally returns true but actually returns NULL PageDescriptor, which subsequently results in OSAddAtomic64(&this->counter) + // __ZN31IGHardwarePerProcessPageTable6421mapDescriptorForRangeERK14IGAddressRangePN10IGPagePool14PageDescriptorE writing to invalid address. + // + // Fault CR2: 0x0000000000000028, Error code: 0x0000000000000002, Fault CPU: 0x1, PL: 0, VF: 0 + // 0xffffff821885b8f0 : 0xffffff801d3c7dc4 mach_kernel : _OSAddAtomic64 + 0x4 + // 0xffffff821885b9e0 : 0xffffff7f9f3f1845 com.apple.driver.AppleIntelKBLGraphics : + // __ZN31IGHardwarePerProcessPageTable6421mapDescriptorForRangeERK14IGAddressRangePN10IGPagePool14PageDescriptorE + 0x6b + // 0xffffff821885ba20 : 0xffffff7f9f40a3c3 com.apple.driver.AppleIntelKBLGraphics : + // __ZN29IGHardwarePerProcessPageTable25synchronizePageDescriptorEPKS_RK14IGAddressRangeb + 0x51 + // 0xffffff821885ba50 : 0xffffff7f9f40a36b com.apple.driver.AppleIntelKBLGraphics : + // __ZN29IGHardwarePerProcessPageTable15synchronizeWithIS_EEvPKT_RK14IGAddressRangeb + 0x37 + // 0xffffff821885ba70 : 0xffffff7f9f3b1716 com.apple.driver.AppleIntelKBLGraphics : __ZN15IGMemoryManager19newPageTableForTaskEP11IGAccelTask + 0x98 + // 0xffffff821885baa0 : 0xffffff7f9f40a716 com.apple.driver.AppleIntelKBLGraphics : __ZN11IGAccelTask15initWithOptionsEP16IntelAccelerator + 0x108 + // 0xffffff821885bad0 : 0xffffff7f9f40a5f7 com.apple.driver.AppleIntelKBLGraphics : __ZN11IGAccelTask11withOptionsEP16IntelAccelerator + 0x31 + // 0xffffff821885baf0 : 0xffffff7f9f3bfaf0 com.apple.driver.AppleIntelKBLGraphics : __ZN16IntelAccelerator17createUserGPUTaskEv + 0x30 + // 0xffffff821885bb10 : 0xffffff7f9f2f7520 com.apple.iokit.IOAcceleratorFamily2 : __ZN14IOAccelShared24initEP22IOGraphicsAccelerator2P4task + 0x2e + // 0xffffff821885bb50 : 0xffffff7f9f30c157 com.apple.iokit.IOAcceleratorFamily2 : __ZN22IOGraphicsAccelerator212createSharedEP4task + 0x33 + // 0xffffff821885bb80 : 0xffffff7f9f2faa95 com.apple.iokit.IOAcceleratorFamily2 : __ZN24IOAccelSharedUserClient211sharedStartEv + 0x2b + // 0xffffff821885bba0 : 0xffffff7f9f401ca2 com.apple.driver.AppleIntelKBLGraphics : __ZN23IGAccelSharedUserClient11sharedStartEv + 0x16 + // 0xffffff821885bbc0 : 0xffffff7f9f2f8aaa com.apple.iokit.IOAcceleratorFamily2 : __ZN24IOAccelSharedUserClient25startEP9IOService + 0x9c + // 0xffffff821885bbf0 : 0xffffff7f9f30ba3c com.apple.iokit.IOAcceleratorFamily2 : __ZN22IOGraphicsAccelerator213newUserClientEP4taskPvjPP12IOUserClient + 0x43e + // 0xffffff821885bc80 : 0xffffff801d42a040 mach_kernel : __ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient + 0x30 + // 0xffffff821885bcd0 : 0xffffff801d48ef9a mach_kernel : _is_io_service_open_extended + 0x10a + // 0xffffff821885bd30 : 0xffffff801ce96b52 mach_kernel : _iokit_server_routine + 0x58d2 + // 0xffffff821885bd80 : 0xffffff801cdb506c mach_kernel : _ipc_kobject_server + 0x12c + // 0xffffff821885bdd0 : 0xffffff801cd8fa91 mach_kernel : _ipc_kmsg_send + 0xd1 + // 0xffffff821885be50 : 0xffffff801cda42fe mach_kernel : _mach_msg_overwrite_trap + 0x3ce + // 0xffffff821885bef0 : 0xffffff801cec3e87 mach_kernel : _mach_call_munger64 + 0x257 + // 0xffffff821885bfa0 : 0xffffff801cd5d2c6 mach_kernel : _hndl_mach_scall64 + 0x16 + // + // Even so the change makes good sense to me, and most likely the real bug is elsewhere. The change workarounds the issue by also checking + // for the W (writeable) bit in addition to P (present). Presumably this works because some code misuses ::read method to iterate + // over page table instead of obtaining valid mapped physical address. + return (flags & 3U) != 0; } bool IGFX::wrapComputeLaneCount(void *that, void *timing, uint32_t bpp, int32_t availableLanes, int32_t *laneCount) { - DBGLOG("igfx", "computeLaneCount: bpp = %u, available = %d", bpp, availableLanes); - - // It seems that AGDP fails to properly detect external boot monitors. As a result computeLaneCount - // is mistakengly called for any boot monitor (e.g. HDMI/DVI), while it is only meant to be used for - // DP (eDP) displays. More details could be found at: - // https://github.com/vit9696/Lilu/issues/27#issuecomment-372103559 - // Since the only problematic function is AppleIntelFramebuffer::validateDetailedTiming, there are - // multiple ways to workaround it. - // 1. In 10.13.4 Apple added an additional extended timing validation call, which happened to be - // guardded by a HDMI 2.0 enable boot-arg, which resulted in one bug fixing the other, and 10.13.5 - // broke it again. - // 2. Another good way is to intercept AppleIntelFramebufferController::RegisterAGDCCallback and - // make sure AppleGraphicsDevicePolicy::_VendorEventHandler returns mode 2 (not 0) for event 10. - // 3. Disabling AGDC by nopping AppleIntelFramebufferController::RegisterAGDCCallback is also fine. - // Simply returning true from computeLaneCount and letting 0 to be compared against zero so far was - // least destructive and most reliable. Let's stick with it until we could solve more problems. - bool r = FunctionCast(wrapComputeLaneCount, callbackIGFX->orgComputeLaneCount)(that, timing, bpp, availableLanes, laneCount); - if (!r && *laneCount == 0) { - DBGLOG("igfx", "reporting worked lane count (legacy)"); - r = true; - } - - return r; + DBGLOG("igfx", "computeLaneCount: bpp = %u, available = %d", bpp, availableLanes); + + // It seems that AGDP fails to properly detect external boot monitors. As a result computeLaneCount + // is mistakengly called for any boot monitor (e.g. HDMI/DVI), while it is only meant to be used for + // DP (eDP) displays. More details could be found at: + // https://github.com/vit9696/Lilu/issues/27#issuecomment-372103559 + // Since the only problematic function is AppleIntelFramebuffer::validateDetailedTiming, there are + // multiple ways to workaround it. + // 1. In 10.13.4 Apple added an additional extended timing validation call, which happened to be + // guardded by a HDMI 2.0 enable boot-arg, which resulted in one bug fixing the other, and 10.13.5 + // broke it again. + // 2. Another good way is to intercept AppleIntelFramebufferController::RegisterAGDCCallback and + // make sure AppleGraphicsDevicePolicy::_VendorEventHandler returns mode 2 (not 0) for event 10. + // 3. Disabling AGDC by nopping AppleIntelFramebufferController::RegisterAGDCCallback is also fine. + // Simply returning true from computeLaneCount and letting 0 to be compared against zero so far was + // least destructive and most reliable. Let's stick with it until we could solve more problems. + bool r = FunctionCast(wrapComputeLaneCount, callbackIGFX->orgComputeLaneCount)(that, timing, bpp, availableLanes, laneCount); + if (!r && *laneCount == 0) { + DBGLOG("igfx", "reporting worked lane count (legacy)"); + r = true; + } + + return r; } bool IGFX::wrapComputeLaneCountNouveau(void *that, void *timing, int32_t availableLanes, int32_t *laneCount) { - bool r = FunctionCast(wrapComputeLaneCountNouveau, callbackIGFX->orgComputeLaneCount)(that, timing, availableLanes, laneCount); - if (!r && *laneCount == 0) { - DBGLOG("igfx", "reporting worked lane count (nouveau)"); - r = true; - } + bool r = FunctionCast(wrapComputeLaneCountNouveau, callbackIGFX->orgComputeLaneCount)(that, timing, availableLanes, laneCount); + if (!r && *laneCount == 0) { + DBGLOG("igfx", "reporting worked lane count (nouveau)"); + r = true; + } - return r; + return r; } OSObject *IGFX::wrapCopyExistingServices(OSDictionary *matching, IOOptionBits inState, IOOptionBits options) { - if (matching && inState == kIOServiceMatchedState && options == 0) { - auto name = OSDynamicCast(OSString, matching->getObject(gIONameMatchKey)); - if (name) { - DBGLOG("igfx", "found matching request by name %s", name->getCStringNoCopy()); - if (name->isEqualTo("Gen6Accelerator")) { - DBGLOG("igfx", "found and fixed Gen6Accelerator request"); - auto accel = OSString::withCString("IntelAccelerator"); - if (accel) { - matching->setObject(gIONameMatchKey, accel); - accel->release(); - } - } - } - } - - return FunctionCast(wrapCopyExistingServices, callbackIGFX->orgCopyExistingServices)(matching, inState, options); + if (matching && inState == kIOServiceMatchedState && options == 0) { + auto name = OSDynamicCast(OSString, matching->getObject(gIONameMatchKey)); + if (name) { + DBGLOG("igfx", "found matching request by name %s", name->getCStringNoCopy()); + if (name->isEqualTo("Gen6Accelerator")) { + DBGLOG("igfx", "found and fixed Gen6Accelerator request"); + auto accel = OSString::withCString("IntelAccelerator"); + if (accel) { + matching->setObject(gIONameMatchKey, accel); + accel->release(); + } + } + } + } + + return FunctionCast(wrapCopyExistingServices, callbackIGFX->orgCopyExistingServices)(matching, inState, options); } bool IGFX::wrapAcceleratorStart(IOService *that, IOService *provider) { - // By default Apple drivers load Apple-specific firmware, which is incompatible. - // On KBL they do it unconditionally, which causes infinite loop. - // On 10.13 there is an option to ignore/load a generic firmware, which we set here. - // On 10.12 it is not necessary. - if (callbackIGFX->avoidFirmwareLoading) { - auto dev = OSDynamicCast(OSDictionary, that->getProperty("Development")); - if (dev && dev->getObject("GraphicsSchedulerSelect")) { - auto newDev = OSDynamicCast(OSDictionary, dev->copyCollection()); - if (newDev) { - // 1 - Automatic scheduler (Apple -> fallback to disabled) - // 2 - Force disable via plist - // 3 - Apple Scheduler - // 4 - Reference Scheduler - auto num = OSNumber::withNumber(callbackIGFX->loadGuCFirmware ? 4 : 2, 32); - if (num) { - newDev->setObject("GraphicsSchedulerSelect", num); - num->release(); - that->setProperty("Development", newDev); - newDev->release(); - } - } - } - } - - if (callbackIGFX->forceOpenGL) { - DBGLOG("igfx", "disabling metal support"); - that->removeProperty("MetalPluginClassName"); - that->removeProperty("MetalPluginName"); - that->removeProperty("MetalStatisticsName"); - } - - if (callbackIGFX->moderniseAccelerator) - that->setName("IntelAccelerator"); - - return FunctionCast(wrapAcceleratorStart, callbackIGFX->orgAcceleratorStart)(that, provider); + // By default Apple drivers load Apple-specific firmware, which is incompatible. + // On KBL they do it unconditionally, which causes infinite loop. + // On 10.13 there is an option to ignore/load a generic firmware, which we set here. + // On 10.12 it is not necessary. + // On 10.15 a different route is used, as GuC firmware loading is completely removed. + if (callbackIGFX->avoidFirmwareLoading && getKernelVersion() <= KernelVersion::Mojave) { + auto dev = OSDynamicCast(OSDictionary, that->getProperty("Development")); + if (dev && dev->getObject("GraphicsSchedulerSelect")) { + auto rawDev = dev->copyCollection(); + if (rawDev) { + auto newDev = OSDynamicCast(OSDictionary, rawDev); + if (newDev) { + // 1 - Automatic scheduler (Apple -> fallback to disabled) + // 2 - Force disable via plist (removed as of 10.15) + // 3 - Apple Scheduler + // 4 - Reference Scheduler + auto num = OSNumber::withNumber(callbackIGFX->loadGuCFirmware ? 4 : 2, 32); + if (num) { + newDev->setObject("GraphicsSchedulerSelect", num); + num->release(); + that->setProperty("Development", newDev); + } + } + rawDev->release(); + } + + } + } + + if (callbackIGFX->forceOpenGL) { + DBGLOG("igfx", "disabling metal support"); + that->removeProperty("MetalPluginClassName"); + that->removeProperty("MetalPluginName"); + that->removeProperty("MetalStatisticsName"); + } + + if (callbackIGFX->moderniseAccelerator) + that->setName("IntelAccelerator"); + + return FunctionCast(wrapAcceleratorStart, callbackIGFX->orgAcceleratorStart)(that, provider); } /** * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer */ int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { - - // - // Abstract: - // - // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of - // an invalid value of maximum link rate reported by DPCD of the builtin display. - // - // One of those fields, namely the number of lanes, is later used as a divisor during - // the link training, resulting in a kernel panic triggered by a divison-by-zero. - // - // DPCD are retrieved from the display via a helper function named ReadAUX(). - // This wrapper function checks whether the driver is reading receiver capabilities - // from DPCD of the builtin display and then provides a custom maximum link rate value, - // so that we don't need to update the binary patch on each system update. - // - // If you are interested in the story behind this fix, take a look at my blog posts. - // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ - // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ - - // Call the original ReadAUX() function to read from DPCD - int retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); - - // Guard: Check the DPCD register address - // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) - if (address != 0) - return retVal; - - // The driver tries to read the first 16 bytes from DPCD - // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) - // We read the value of "IOFBDependentIndex" instead of accessing that field directly - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - - // Guard: Should be able to retrieve the index from the registry - if (!index) { - SYSLOG("igfx", "MLR: wrapReadAUX: Failed to read the current framebuffer index."); - return retVal; - } - - // Guard: Check the framebuffer index - // By default, FB 0 refers the builtin display - if (index->unsigned32BitValue() != 0) - // The driver is reading DPCD for an external display - return retVal; - - // The driver tries to read the receiver capabilities for the builtin display - auto caps = reinterpret_cast(buffer); - - // Set the custom maximum link rate value - caps->maxLinkRate = callbackIGFX->maxLinkRate; - - DBGLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); - - return retVal; + + // + // Abstract: + // + // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of + // an invalid value of maximum link rate reported by DPCD of the builtin display. + // + // One of those fields, namely the number of lanes, is later used as a divisor during + // the link training, resulting in a kernel panic triggered by a divison-by-zero. + // + // DPCD are retrieved from the display via a helper function named ReadAUX(). + // This wrapper function checks whether the driver is reading receiver capabilities + // from DPCD of the builtin display and then provides a custom maximum link rate value, + // so that we don't need to update the binary patch on each system update. + // + // If you are interested in the story behind this fix, take a look at my blog posts. + // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + + // Call the original ReadAUX() function to read from DPCD + int retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); + + // Guard: Check the DPCD register address + // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) + if (address != 0) + return retVal; + + // The driver tries to read the first 16 bytes from DPCD + // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) + // We read the value of "IOFBDependentIndex" instead of accessing that field directly + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + + // Guard: Should be able to retrieve the index from the registry + if (!index) { + SYSLOG("igfx", "MLR: wrapReadAUX: Failed to read the current framebuffer index."); + return retVal; + } + + // Guard: Check the framebuffer index + // By default, FB 0 refers the builtin display + if (index->unsigned32BitValue() != 0) + // The driver is reading DPCD for an external display + return retVal; + + // The driver tries to read the receiver capabilities for the builtin display + auto caps = reinterpret_cast(buffer); + + // Set the custom maximum link rate value + caps->maxLinkRate = callbackIGFX->maxLinkRate; + + DBGLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); + + return retVal; } void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { - if (reg == BXT_BLC_PWM_FREQ1) { - if (value && value != callbackIGFX->driverBacklightFrequency) { - DBGLOG("igfx", "wrapCflWriteRegister32: driver requested BXT_BLC_PWM_FREQ1 = 0x%x", value); - callbackIGFX->driverBacklightFrequency = value; - } - - if (callbackIGFX->targetBacklightFrequency == 0) { - // Save the hardware PWM frequency as initially set up by the system firmware. - // We'll need this to restore later after system sleep. - callbackIGFX->targetBacklightFrequency = callbackIGFX->orgCflReadRegister32(that, BXT_BLC_PWM_FREQ1); - DBGLOG("igfx", "wrapCflWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 = 0x%x", callbackIGFX->targetBacklightFrequency); - - if (callbackIGFX->targetBacklightFrequency == 0) { - // This should not happen with correctly written bootloader code, but in case it does, let's use a failsafe default value. - callbackIGFX->targetBacklightFrequency = FallbackTargetBacklightFrequency; - SYSLOG("igfx", "wrapCflWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 is ZERO"); - } - } - - if (value) { - // Nonzero writes to this register need to use the original system value. - // Yet the driver can safely write zero to this register as part of system sleep. - value = callbackIGFX->targetBacklightFrequency; - } - } else if (reg == BXT_BLC_PWM_DUTY1) { - if (callbackIGFX->driverBacklightFrequency && callbackIGFX->targetBacklightFrequency) { - // Translate the PWM duty cycle between the driver scale value and the HW scale value - uint32_t rescaledValue = static_cast((value * static_cast(callbackIGFX->targetBacklightFrequency)) / - static_cast(callbackIGFX->driverBacklightFrequency)); - DBGLOG("igfx", "wrapCflWriteRegister32: write PWM_DUTY1 0x%x/0x%x, rescaled to 0x%x/0x%x", value, - callbackIGFX->driverBacklightFrequency, rescaledValue, callbackIGFX->targetBacklightFrequency); - value = rescaledValue; - } else { - // This should never happen, but in case it does we should log it at the very least. - SYSLOG("igfx", "wrapCflWriteRegister32: write PWM_DUTY1 has zero frequency driver (%d) target (%d)", - callbackIGFX->driverBacklightFrequency, callbackIGFX->targetBacklightFrequency); - } - } - - callbackIGFX->orgCflWriteRegister32(that, reg, value); + if (reg == BXT_BLC_PWM_FREQ1) { + if (value && value != callbackIGFX->driverBacklightFrequency) { + DBGLOG("igfx", "wrapCflWriteRegister32: driver requested BXT_BLC_PWM_FREQ1 = 0x%x", value); + callbackIGFX->driverBacklightFrequency = value; + } + + if (callbackIGFX->targetBacklightFrequency == 0) { + // Save the hardware PWM frequency as initially set up by the system firmware. + // We'll need this to restore later after system sleep. + callbackIGFX->targetBacklightFrequency = callbackIGFX->orgCflReadRegister32(that, BXT_BLC_PWM_FREQ1); + DBGLOG("igfx", "wrapCflWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 = 0x%x", callbackIGFX->targetBacklightFrequency); + + if (callbackIGFX->targetBacklightFrequency == 0) { + // This should not happen with correctly written bootloader code, but in case it does, let's use a failsafe default value. + callbackIGFX->targetBacklightFrequency = FallbackTargetBacklightFrequency; + SYSLOG("igfx", "wrapCflWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 is ZERO"); + } + } + + if (value) { + // Nonzero writes to this register need to use the original system value. + // Yet the driver can safely write zero to this register as part of system sleep. + value = callbackIGFX->targetBacklightFrequency; + } + } else if (reg == BXT_BLC_PWM_DUTY1) { + if (callbackIGFX->driverBacklightFrequency && callbackIGFX->targetBacklightFrequency) { + // Translate the PWM duty cycle between the driver scale value and the HW scale value + uint32_t rescaledValue = static_cast((value * static_cast(callbackIGFX->targetBacklightFrequency)) / + static_cast(callbackIGFX->driverBacklightFrequency)); + DBGLOG("igfx", "wrapCflWriteRegister32: write PWM_DUTY1 0x%x/0x%x, rescaled to 0x%x/0x%x", value, + callbackIGFX->driverBacklightFrequency, rescaledValue, callbackIGFX->targetBacklightFrequency); + value = rescaledValue; + } else { + // This should never happen, but in case it does we should log it at the very least. + SYSLOG("igfx", "wrapCflWriteRegister32: write PWM_DUTY1 has zero frequency driver (%d) target (%d)", + callbackIGFX->driverBacklightFrequency, callbackIGFX->targetBacklightFrequency); + } + } + + callbackIGFX->orgCflWriteRegister32(that, reg, value); } void IGFX::wrapKblWriteRegister32(void *that, uint32_t reg, uint32_t value) { - if (reg == BXT_BLC_PWM_FREQ1) { // aka BLC_PWM_PCH_CTL2 - if (callbackIGFX->targetBacklightFrequency == 0) { - // Populate the hardware PWM frequency as initially set up by the system firmware. - callbackIGFX->targetBacklightFrequency = callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_FREQ1); - DBGLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 = 0x%x", callbackIGFX->targetBacklightFrequency); - DBGLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_CTL1 = 0x%x", callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1)); - - if (callbackIGFX->targetBacklightFrequency == 0) { - // This should not happen with correctly written bootloader code, but in case it does, let's use a failsafe default value. - callbackIGFX->targetBacklightFrequency = FallbackTargetBacklightFrequency; - SYSLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 is ZERO"); - } - } - - // For the KBL driver, 0xc8254 (BLC_PWM_PCH_CTL2) controls the backlight intensity. - // High 16 of this write are the denominator (frequency), low 16 are the numerator (duty cycle). - // Translate this into a write to c8258 (BXT_BLC_PWM_DUTY1) for the CFL hardware, scaled by the system-provided value in c8254 (BXT_BLC_PWM_FREQ1). - uint16_t frequency = (value & 0xffff0000U) >> 16U; - uint16_t dutyCycle = value & 0xffffU; - - uint32_t rescaledValue = frequency == 0 ? 0 : static_cast((dutyCycle * static_cast(callbackIGFX->targetBacklightFrequency)) / - static_cast(frequency)); - DBGLOG("igfx", "wrapKblWriteRegister32: write PWM_DUTY1 0x%x/0x%x, rescaled to 0x%x/0x%x", - dutyCycle, frequency, rescaledValue, callbackIGFX->targetBacklightFrequency); - - // Reset the hardware PWM frequency. Write the original system value if the driver-requested value is nonzero. If the driver requests - // zero, we allow that, since it's trying to turn off the backlight PWM for sleep. - callbackIGFX->orgKblWriteRegister32(that, BXT_BLC_PWM_FREQ1, frequency ? callbackIGFX->targetBacklightFrequency : 0); - - // Finish by writing the duty cycle. - reg = BXT_BLC_PWM_DUTY1; - value = rescaledValue; - } else if (reg == BXT_BLC_PWM_CTL1) { - if (callbackIGFX->targetPwmControl == 0) { - // Save the original hardware PWM control value - callbackIGFX->targetPwmControl = callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1); - } - - DBGLOG("igfx", "wrapKblWriteRegister32: write BXT_BLC_PWM_CTL1 0x%x, previous was 0x%x", value, callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1)); - - if (value) { - // Set the PWM frequency before turning it on to avoid the 3 minute blackout bug - callbackIGFX->orgKblWriteRegister32(that, BXT_BLC_PWM_FREQ1, callbackIGFX->targetBacklightFrequency); - - // Use the original hardware PWM control value. - value = callbackIGFX->targetPwmControl; - } - } - - callbackIGFX->orgKblWriteRegister32(that, reg, value); + if (reg == BXT_BLC_PWM_FREQ1) { // aka BLC_PWM_PCH_CTL2 + if (callbackIGFX->targetBacklightFrequency == 0) { + // Populate the hardware PWM frequency as initially set up by the system firmware. + callbackIGFX->targetBacklightFrequency = callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_FREQ1); + DBGLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 = 0x%x", callbackIGFX->targetBacklightFrequency); + DBGLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_CTL1 = 0x%x", callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1)); + + if (callbackIGFX->targetBacklightFrequency == 0) { + // This should not happen with correctly written bootloader code, but in case it does, let's use a failsafe default value. + callbackIGFX->targetBacklightFrequency = FallbackTargetBacklightFrequency; + SYSLOG("igfx", "wrapKblWriteRegister32: system initialized BXT_BLC_PWM_FREQ1 is ZERO"); + } + } + + // For the KBL driver, 0xc8254 (BLC_PWM_PCH_CTL2) controls the backlight intensity. + // High 16 of this write are the denominator (frequency), low 16 are the numerator (duty cycle). + // Translate this into a write to c8258 (BXT_BLC_PWM_DUTY1) for the CFL hardware, scaled by the system-provided value in c8254 (BXT_BLC_PWM_FREQ1). + uint16_t frequency = (value & 0xffff0000U) >> 16U; + uint16_t dutyCycle = value & 0xffffU; + + uint32_t rescaledValue = frequency == 0 ? 0 : static_cast((dutyCycle * static_cast(callbackIGFX->targetBacklightFrequency)) / + static_cast(frequency)); + DBGLOG("igfx", "wrapKblWriteRegister32: write PWM_DUTY1 0x%x/0x%x, rescaled to 0x%x/0x%x", + dutyCycle, frequency, rescaledValue, callbackIGFX->targetBacklightFrequency); + + // Reset the hardware PWM frequency. Write the original system value if the driver-requested value is nonzero. If the driver requests + // zero, we allow that, since it's trying to turn off the backlight PWM for sleep. + callbackIGFX->orgKblWriteRegister32(that, BXT_BLC_PWM_FREQ1, frequency ? callbackIGFX->targetBacklightFrequency : 0); + + // Finish by writing the duty cycle. + reg = BXT_BLC_PWM_DUTY1; + value = rescaledValue; + } else if (reg == BXT_BLC_PWM_CTL1) { + if (callbackIGFX->targetPwmControl == 0) { + // Save the original hardware PWM control value + callbackIGFX->targetPwmControl = callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1); + } + + DBGLOG("igfx", "wrapKblWriteRegister32: write BXT_BLC_PWM_CTL1 0x%x, previous was 0x%x", value, callbackIGFX->orgKblReadRegister32(that, BXT_BLC_PWM_CTL1)); + + if (value) { + // Set the PWM frequency before turning it on to avoid the 3 minute blackout bug + callbackIGFX->orgKblWriteRegister32(that, BXT_BLC_PWM_FREQ1, callbackIGFX->targetBacklightFrequency); + + // Use the original hardware PWM control value. + value = callbackIGFX->targetPwmControl; + } + } + + callbackIGFX->orgKblWriteRegister32(that, reg, value); } bool IGFX::wrapGetOSInformation(void *that) { #ifdef DEBUG - if (callbackIGFX->dumpFramebufferToDisk) { - char name[64]; - snprintf(name, sizeof(name), "/AppleIntelFramebuffer_%d_%d.%d", callbackIGFX->cpuGeneration, getKernelVersion(), getKernelMinorVersion()); - FileIO::writeBufferToFile(name, callbackIGFX->framebufferStart, callbackIGFX->framebufferSize); - SYSLOG("igfx", "dumping framebuffer information to %s", name); - } + if (callbackIGFX->dumpFramebufferToDisk) { + char name[64]; + snprintf(name, sizeof(name), "/AppleIntelFramebuffer_%d_%d.%d", callbackIGFX->cpuGeneration, getKernelVersion(), getKernelMinorVersion()); + FileIO::writeBufferToFile(name, callbackIGFX->framebufferStart, callbackIGFX->framebufferSize); + SYSLOG("igfx", "dumping framebuffer information to %s", name); + } #endif #ifdef DEBUG - if (callbackIGFX->dumpPlatformTable) - callbackIGFX->writePlatformListData("platform-table-native"); + if (callbackIGFX->dumpPlatformTable) + callbackIGFX->writePlatformListData("platform-table-native"); #endif - if (callbackIGFX->applyFramebufferPatch) - callbackIGFX->applyFramebufferPatches(); - else if (callbackIGFX->hdmiAutopatch) - callbackIGFX->applyHdmiAutopatch(); + if (callbackIGFX->applyFramebufferPatch) + callbackIGFX->applyFramebufferPatches(); + else if (callbackIGFX->hdmiAutopatch) + callbackIGFX->applyHdmiAutopatch(); #ifdef DEBUG - if (callbackIGFX->dumpPlatformTable) - callbackIGFX->writePlatformListData("platform-table-patched"); + if (callbackIGFX->dumpPlatformTable) + callbackIGFX->writePlatformListData("platform-table-patched"); #endif - return FunctionCast(wrapGetOSInformation, callbackIGFX->orgGetOSInformation)(that); + return FunctionCast(wrapGetOSInformation, callbackIGFX->orgGetOSInformation)(that); } bool IGFX::wrapLoadGuCBinary(void *that, bool flag) { - bool r = false; - DBGLOG("igfx", "attempting to load firmware for %d scheduler for cpu gen %d", - callbackIGFX->loadGuCFirmware, callbackIGFX->cpuGeneration); + bool r = false; + DBGLOG("igfx", "attempting to load firmware for %d scheduler for cpu gen %d", + callbackIGFX->loadGuCFirmware, callbackIGFX->cpuGeneration); - if (callbackIGFX->firmwareSizePointer) - callbackIGFX->performingFirmwareLoad = true; + if (callbackIGFX->firmwareSizePointer) + callbackIGFX->performingFirmwareLoad = true; - r = FunctionCast(wrapLoadGuCBinary, callbackIGFX->orgLoadGuCBinary)(that, flag); - DBGLOG("igfx", "loadGuCBinary returned %d", r); + r = FunctionCast(wrapLoadGuCBinary, callbackIGFX->orgLoadGuCBinary)(that, flag); + DBGLOG("igfx", "loadGuCBinary returned %d", r); - callbackIGFX->performingFirmwareLoad = false; + callbackIGFX->performingFirmwareLoad = false; - return r; + return r; } bool IGFX::wrapLoadFirmware(IOService *that) { - DBGLOG("igfx", "load firmware setting sleep overrides %d", callbackIGFX->cpuGeneration); - - // We have to patch the virtual table, because the original methods are very short. - // See __ZN12IGScheduler415systemWillSleepEv and __ZN12IGScheduler413systemDidWakeEv - // Note, that other methods are also not really implemented, so we may have to implement them ourselves sooner or later. - (*reinterpret_cast(that))[52] = reinterpret_cast(wrapSystemWillSleep); - (*reinterpret_cast(that))[53] = reinterpret_cast(wrapSystemDidWake); - return FunctionCast(wrapLoadFirmware, callbackIGFX->orgLoadFirmware)(that); + DBGLOG("igfx", "load firmware setting sleep overrides %d", callbackIGFX->cpuGeneration); + + // We have to patch the virtual table, because the original methods are very short. + // See __ZN12IGScheduler415systemWillSleepEv and __ZN12IGScheduler413systemDidWakeEv + // Note, that other methods are also not really implemented, so we may have to implement them ourselves sooner or later. + (*reinterpret_cast(that))[52] = reinterpret_cast(wrapSystemWillSleep); + (*reinterpret_cast(that))[53] = reinterpret_cast(wrapSystemDidWake); + return FunctionCast(wrapLoadFirmware, callbackIGFX->orgLoadFirmware)(that); } void IGFX::wrapSystemWillSleep(IOService *that) { - DBGLOG("igfx", "systemWillSleep GuC callback"); - // Perhaps we want to send a message to GuC firmware like Apple does for its own implementation? + DBGLOG("igfx", "systemWillSleep GuC callback"); + // Perhaps we want to send a message to GuC firmware like Apple does for its own implementation? } void IGFX::wrapSystemDidWake(IOService *that) { - DBGLOG("igfx", "systemDidWake GuC callback"); + DBGLOG("igfx", "systemDidWake GuC callback"); - // This is IGHardwareGuC class instance. - auto &GuC = (reinterpret_cast(that))[76]; - DBGLOG("igfx", "reloading firmware on wake discovered IGHardwareGuC %d", GuC != nullptr); - if (GuC) { - GuC->release(); - GuC = nullptr; - } + // This is IGHardwareGuC class instance. + auto &GuC = (reinterpret_cast(that))[76]; + DBGLOG("igfx", "reloading firmware on wake discovered IGHardwareGuC %d", GuC != nullptr); + if (GuC) { + GuC->release(); + GuC = nullptr; + } - FunctionCast(wrapLoadFirmware, callbackIGFX->orgLoadFirmware)(that); + FunctionCast(wrapLoadFirmware, callbackIGFX->orgLoadFirmware)(that); } bool IGFX::wrapInitSchedControl(void *that, void *ctrl) { - // This function is caled within loadGuCBinary, and it also uses shared buffers. - // To avoid any issues here we ensure that the filtering is off. - DBGLOG("igfx", "attempting to init sched control with load %d", callbackIGFX->performingFirmwareLoad); - bool perfLoad = callbackIGFX->performingFirmwareLoad; - callbackIGFX->performingFirmwareLoad = false; - bool r = FunctionCast(wrapInitSchedControl, callbackIGFX->orgInitSchedControl)(that, ctrl); + // This function is caled within loadGuCBinary, and it also uses shared buffers. + // To avoid any issues here we ensure that the filtering is off. + DBGLOG("igfx", "attempting to init sched control with load %d", callbackIGFX->performingFirmwareLoad); + bool perfLoad = callbackIGFX->performingFirmwareLoad; + callbackIGFX->performingFirmwareLoad = false; + bool r = FunctionCast(wrapInitSchedControl, callbackIGFX->orgInitSchedControl)(that, ctrl); #ifdef DEBUG - if (callbackIGFX->loadGuCFirmware) { - struct ParamRegs { - uint32_t bak[35]; - uint32_t params[10]; - }; - - auto v = &static_cast(that)->params[0]; - DBGLOG("igfx", "fw params1 %08X %08X %08X %08X %08X", v[0], v[1], v[2], v[3], v[4]); - DBGLOG("igfx", "fw params2 %08X %08X %08X %08X %08X", v[5], v[6], v[7], v[8], v[9]); - } + if (callbackIGFX->loadGuCFirmware) { + struct ParamRegs { + uint32_t bak[35]; + uint32_t params[10]; + }; + + auto v = &static_cast(that)->params[0]; + DBGLOG("igfx", "fw params1 %08X %08X %08X %08X %08X", v[0], v[1], v[2], v[3], v[4]); + DBGLOG("igfx", "fw params2 %08X %08X %08X %08X %08X", v[5], v[6], v[7], v[8], v[9]); + } #endif - callbackIGFX->performingFirmwareLoad = perfLoad; - return r; + callbackIGFX->performingFirmwareLoad = perfLoad; + return r; } void *IGFX::wrapIgBufferWithOptions(void *accelTask, unsigned long size, unsigned int type, unsigned int flags) { - void *r = nullptr; - - if (callbackIGFX->performingFirmwareLoad) { - // Allocate a dummy buffer - callbackIGFX->dummyFirmwareBuffer = Buffer::create(size); - // Select the latest firmware to upload - DBGLOG("igfx", "preparing firmware for cpu gen %d with range 0x%lX", callbackIGFX->cpuGeneration, size); - - const void *fw = nullptr; - const void *fwsig = nullptr; - size_t fwsize = 0; - size_t fwsigsize = 0; - - if (callbackIGFX->cpuGeneration == CPUInfo::CpuGeneration::Skylake) { - fw = GuCFirmwareSKL; - fwsig = GuCFirmwareSKLSignature; - fwsize = GuCFirmwareSKLSize; - fwsigsize = GuCFirmwareSignatureSize; - } else { - fw = GuCFirmwareKBL; - fwsig = GuCFirmwareKBLSignature; - fwsize = GuCFirmwareKBLSize; - fwsigsize = GuCFirmwareSignatureSize; - } - - // Allocate enough memory for the new firmware (should be 64K-aligned) - unsigned long newsize = fwsize > size ? ((fwsize + 0xFFFF) & (~0xFFFF)) : size; - r = FunctionCast(wrapIgBufferWithOptions, callbackIGFX->orgIgBufferWithOptions)(accelTask, newsize, type, flags); - // Replace the real buffer with a dummy buffer - if (r && callbackIGFX->dummyFirmwareBuffer) { - // Copy firmware contents, update the sizes and signature - auto status = MachInfo::setKernelWriting(true, KernelPatcher::kernelWriteLock); - if (status == KERN_SUCCESS) { - // Upload the firmware ourselves - callbackIGFX->realFirmwareBuffer = static_cast(r)[7]; - static_cast(r)[7] = callbackIGFX->dummyFirmwareBuffer; - lilu_os_memcpy(callbackIGFX->realFirmwareBuffer, fw, fwsize); - lilu_os_memcpy(callbackIGFX->signaturePointer, fwsig, fwsigsize); - callbackIGFX->realBinarySize = static_cast(fwsize); - // Update the firmware size for IGScheduler4 - *callbackIGFX->firmwareSizePointer = static_cast(fwsize); - MachInfo::setKernelWriting(false, KernelPatcher::kernelWriteLock); - } else { - SYSLOG("igfx", "ig buffer protection upgrade failure %d", status); - } - } else if (callbackIGFX->dummyFirmwareBuffer) { - SYSLOG("igfx", "ig shared buffer allocation failure"); - Buffer::deleter(callbackIGFX->dummyFirmwareBuffer); - callbackIGFX->dummyFirmwareBuffer = nullptr; - } else { - SYSLOG("igfx", "dummy buffer allocation failure"); - } - } else { - r = FunctionCast(wrapIgBufferWithOptions, callbackIGFX->orgIgBufferWithOptions)(accelTask, size, type, flags); - } - - return r; + void *r = nullptr; + + if (callbackIGFX->performingFirmwareLoad) { + // Allocate a dummy buffer + callbackIGFX->dummyFirmwareBuffer = Buffer::create(size); + // Select the latest firmware to upload + DBGLOG("igfx", "preparing firmware for cpu gen %d with range 0x%lX", callbackIGFX->cpuGeneration, size); + + const void *fw = nullptr; + const void *fwsig = nullptr; + size_t fwsize = 0; + size_t fwsigsize = 0; + + if (callbackIGFX->cpuGeneration == CPUInfo::CpuGeneration::Skylake) { + fw = GuCFirmwareSKL; + fwsig = GuCFirmwareSKLSignature; + fwsize = GuCFirmwareSKLSize; + fwsigsize = GuCFirmwareSignatureSize; + } else { + fw = GuCFirmwareKBL; + fwsig = GuCFirmwareKBLSignature; + fwsize = GuCFirmwareKBLSize; + fwsigsize = GuCFirmwareSignatureSize; + } + + // Allocate enough memory for the new firmware (should be 64K-aligned) + unsigned long newsize = fwsize > size ? ((fwsize + 0xFFFF) & (~0xFFFF)) : size; + r = FunctionCast(wrapIgBufferWithOptions, callbackIGFX->orgIgBufferWithOptions)(accelTask, newsize, type, flags); + // Replace the real buffer with a dummy buffer + if (r && callbackIGFX->dummyFirmwareBuffer) { + // Copy firmware contents, update the sizes and signature + auto status = MachInfo::setKernelWriting(true, KernelPatcher::kernelWriteLock); + if (status == KERN_SUCCESS) { + // Upload the firmware ourselves + callbackIGFX->realFirmwareBuffer = static_cast(r)[7]; + static_cast(r)[7] = callbackIGFX->dummyFirmwareBuffer; + lilu_os_memcpy(callbackIGFX->realFirmwareBuffer, fw, fwsize); + lilu_os_memcpy(callbackIGFX->signaturePointer, fwsig, fwsigsize); + callbackIGFX->realBinarySize = static_cast(fwsize); + // Update the firmware size for IGScheduler4 + *callbackIGFX->firmwareSizePointer = static_cast(fwsize); + MachInfo::setKernelWriting(false, KernelPatcher::kernelWriteLock); + } else { + SYSLOG("igfx", "ig buffer protection upgrade failure %d", status); + } + } else if (callbackIGFX->dummyFirmwareBuffer) { + SYSLOG("igfx", "ig shared buffer allocation failure"); + Buffer::deleter(callbackIGFX->dummyFirmwareBuffer); + callbackIGFX->dummyFirmwareBuffer = nullptr; + } else { + SYSLOG("igfx", "dummy buffer allocation failure"); + } + } else { + r = FunctionCast(wrapIgBufferWithOptions, callbackIGFX->orgIgBufferWithOptions)(accelTask, size, type, flags); + } + + return r; } uint64_t IGFX::wrapIgBufferGetGpuVirtualAddress(void *that) { - if (callbackIGFX->performingFirmwareLoad && callbackIGFX->realFirmwareBuffer) { - // Restore the original firmware buffer - static_cast(that)[7] = callbackIGFX->realFirmwareBuffer; - callbackIGFX->realFirmwareBuffer = nullptr; - // Free the dummy framebuffer which is no longer used - Buffer::deleter(callbackIGFX->dummyFirmwareBuffer); - callbackIGFX->dummyFirmwareBuffer = nullptr; - } - - return FunctionCast(wrapIgBufferGetGpuVirtualAddress, callbackIGFX->orgIgBufferGetGpuVirtualAddress)(that); + if (callbackIGFX->performingFirmwareLoad && callbackIGFX->realFirmwareBuffer) { + // Restore the original firmware buffer + static_cast(that)[7] = callbackIGFX->realFirmwareBuffer; + callbackIGFX->realFirmwareBuffer = nullptr; + // Free the dummy framebuffer which is no longer used + Buffer::deleter(callbackIGFX->dummyFirmwareBuffer); + callbackIGFX->dummyFirmwareBuffer = nullptr; + } + + return FunctionCast(wrapIgBufferGetGpuVirtualAddress, callbackIGFX->orgIgBufferGetGpuVirtualAddress)(that); } void IGFX::loadIGScheduler4Patches(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - gKmGen9GuCBinary = patcher.solveSymbol(index, "__ZL17__KmGen9GuCBinary", address, size); - if (gKmGen9GuCBinary) { - DBGLOG("igfx", "obtained __KmGen9GuCBinary"); - - auto loadGuC = patcher.solveSymbol(index, "__ZN13IGHardwareGuC13loadGuCBinaryEv", address, size); - if (loadGuC) { - DBGLOG("igfx", "obtained IGHardwareGuC::loadGuCBinary"); - - // Lookup the assignment to the size register. - uint8_t sizeReg[] {0x10, 0xC3, 0x00, 0x00}; - auto pos = reinterpret_cast(loadGuC); - auto endPos = pos + PAGE_SIZE; - while (memcmp(pos, sizeReg, sizeof(sizeReg)) != 0 && pos < endPos) - pos++; - - // Verify and store the size pointer - if (pos != endPos) { - pos += sizeof(uint32_t); - firmwareSizePointer = reinterpret_cast(pos); - DBGLOG("igfx", "discovered firmware size: %u bytes", *firmwareSizePointer); - // Firmware size must not be bigger than 1 MB - if ((*firmwareSizePointer & 0xFFFFF) == *firmwareSizePointer) - // Firmware follows the signature - signaturePointer = gKmGen9GuCBinary + *firmwareSizePointer; - else - firmwareSizePointer = nullptr; - } - - if (firmwareSizePointer) { - orgLoadGuCBinary = patcher.routeFunction(loadGuC, reinterpret_cast(wrapLoadGuCBinary), true); - if (patcher.getError() == KernelPatcher::Error::NoError) { - DBGLOG("igfx", "routed IGHardwareGuC::loadGuCBinary"); - - KernelPatcher::RouteRequest requests[] { - {"__ZN12IGScheduler412loadFirmwareEv", wrapLoadFirmware, orgLoadFirmware}, - {"__ZN13IGHardwareGuC16initSchedControlEv", wrapInitSchedControl, orgInitSchedControl}, - {"__ZN20IGSharedMappedBuffer11withOptionsEP11IGAccelTaskmjj", wrapIgBufferWithOptions, orgIgBufferWithOptions}, - {"__ZNK14IGMappedBuffer20getGPUVirtualAddressEv", wrapIgBufferGetGpuVirtualAddress, orgIgBufferGetGpuVirtualAddress}, - }; - patcher.routeMultiple(index, requests, address, size); - } else { - SYSLOG("igfx", "failed to route IGHardwareGuC::loadGuCBinary %d", patcher.getError()); - patcher.clearError(); - } - } else { - SYSLOG("igfx", "failed to find GuC firmware size assignment"); - } - } else { - SYSLOG("igfx", "failed to resolve IGHardwareGuC::loadGuCBinary %d", patcher.getError()); - patcher.clearError(); - } - } else { - SYSLOG("igfx", "failed to resoolve __KmGen9GuCBinary %d", patcher.getError()); - patcher.clearError(); - } + gKmGen9GuCBinary = patcher.solveSymbol(index, "__ZL17__KmGen9GuCBinary", address, size); + if (gKmGen9GuCBinary) { + DBGLOG("igfx", "obtained __KmGen9GuCBinary"); + + auto loadGuC = patcher.solveSymbol(index, "__ZN13IGHardwareGuC13loadGuCBinaryEv", address, size); + if (loadGuC) { + DBGLOG("igfx", "obtained IGHardwareGuC::loadGuCBinary"); + + // Lookup the assignment to the size register. + uint8_t sizeReg[] {0x10, 0xC3, 0x00, 0x00}; + auto pos = reinterpret_cast(loadGuC); + auto endPos = pos + PAGE_SIZE; + while (memcmp(pos, sizeReg, sizeof(sizeReg)) != 0 && pos < endPos) + pos++; + + // Verify and store the size pointer + if (pos != endPos) { + pos += sizeof(uint32_t); + firmwareSizePointer = reinterpret_cast(pos); + DBGLOG("igfx", "discovered firmware size: %u bytes", *firmwareSizePointer); + // Firmware size must not be bigger than 1 MB + if ((*firmwareSizePointer & 0xFFFFF) == *firmwareSizePointer) + // Firmware follows the signature + signaturePointer = gKmGen9GuCBinary + *firmwareSizePointer; + else + firmwareSizePointer = nullptr; + } + + if (firmwareSizePointer) { + orgLoadGuCBinary = patcher.routeFunction(loadGuC, reinterpret_cast(wrapLoadGuCBinary), true); + if (patcher.getError() == KernelPatcher::Error::NoError) { + DBGLOG("igfx", "routed IGHardwareGuC::loadGuCBinary"); + + KernelPatcher::RouteRequest requests[] { + {"__ZN12IGScheduler412loadFirmwareEv", wrapLoadFirmware, orgLoadFirmware}, + {"__ZN13IGHardwareGuC16initSchedControlEv", wrapInitSchedControl, orgInitSchedControl}, + {"__ZN20IGSharedMappedBuffer11withOptionsEP11IGAccelTaskmjj", wrapIgBufferWithOptions, orgIgBufferWithOptions}, + {"__ZNK14IGMappedBuffer20getGPUVirtualAddressEv", wrapIgBufferGetGpuVirtualAddress, orgIgBufferGetGpuVirtualAddress}, + }; + patcher.routeMultiple(index, requests, address, size); + } else { + SYSLOG("igfx", "failed to route IGHardwareGuC::loadGuCBinary %d", patcher.getError()); + patcher.clearError(); + } + } else { + SYSLOG("igfx", "failed to find GuC firmware size assignment"); + } + } else { + SYSLOG("igfx", "failed to resolve IGHardwareGuC::loadGuCBinary %d", patcher.getError()); + patcher.clearError(); + } + } else { + SYSLOG("igfx", "failed to resoolve __KmGen9GuCBinary %d", patcher.getError()); + patcher.clearError(); + } } bool IGFX::loadPatchesFromDevice(IORegistryEntry *igpu, uint32_t currentFramebufferId) { - bool hasFramebufferPatch = false; - - uint32_t framebufferPatchEnable = 0; - if (WIOKit::getOSDataValue(igpu, "framebuffer-patch-enable", framebufferPatchEnable) && framebufferPatchEnable) { - DBGLOG("igfx", "framebuffer-patch-enable %d", framebufferPatchEnable); - - // Note, the casts to uint32_t here and below are required due to device properties always injecting 32-bit types. - framebufferPatchFlags.bits.FPFFramebufferId = WIOKit::getOSDataValue(igpu, "framebuffer-framebufferid", framebufferPatch.framebufferId); - framebufferPatchFlags.bits.FPFFlags = WIOKit::getOSDataValue(igpu, "framebuffer-flags", framebufferPatch.flags.value); - framebufferPatchFlags.bits.FPFCamelliaVersion = WIOKit::getOSDataValue(igpu, "framebuffer-camellia", framebufferPatch.camelliaVersion); - framebufferPatchFlags.bits.FPFMobile = WIOKit::getOSDataValue(igpu, "framebuffer-mobile", framebufferPatch.fMobile); - framebufferPatchFlags.bits.FPFPipeCount = WIOKit::getOSDataValue(igpu, "framebuffer-pipecount", framebufferPatch.fPipeCount); - framebufferPatchFlags.bits.FPFPortCount = WIOKit::getOSDataValue(igpu, "framebuffer-portcount", framebufferPatch.fPortCount); - framebufferPatchFlags.bits.FPFFBMemoryCount = WIOKit::getOSDataValue(igpu, "framebuffer-memorycount", framebufferPatch.fFBMemoryCount); - framebufferPatchFlags.bits.FPFStolenMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-stolenmem", framebufferPatch.fStolenMemorySize); - framebufferPatchFlags.bits.FPFFramebufferMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-fbmem", framebufferPatch.fFramebufferMemorySize); - framebufferPatchFlags.bits.FPFUnifiedMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-unifiedmem", framebufferPatch.fUnifiedMemorySize); - framebufferPatchFlags.bits.FPFFramebufferCursorSize = WIOKit::getOSDataValue(igpu, "framebuffer-cursormem", fPatchCursorMemorySize); - - if (framebufferPatchFlags.value != 0) - hasFramebufferPatch = true; - - for (size_t i = 0; i < arrsize(framebufferPatch.connectors); i++) { - char name[48]; - snprintf(name, sizeof(name), "framebuffer-con%lu-enable", i); - uint32_t framebufferConnectorPatchEnable = 0; - if (!WIOKit::getOSDataValue(igpu, name, framebufferConnectorPatchEnable) || !framebufferConnectorPatchEnable) - continue; - - DBGLOG("igfx", "framebuffer-con%lu-enable %d", i, framebufferConnectorPatchEnable); - - snprintf(name, sizeof(name), "framebuffer-con%lu-%08x-alldata", i, currentFramebufferId); - auto allData = OSDynamicCast(OSData, igpu->getProperty(name)); - if (!allData) { - snprintf(name, sizeof(name), "framebuffer-con%lu-alldata", i); - allData = OSDynamicCast(OSData, igpu->getProperty(name)); - } - if (allData) { - auto allDataSize = allData->getLength(); - auto replaceCount = allDataSize / sizeof(framebufferPatch.connectors[0]); - if (0 == allDataSize % sizeof(framebufferPatch.connectors[0]) && i + replaceCount <= arrsize(framebufferPatch.connectors)) { - auto replacementConnectors = reinterpret_cast(allData->getBytesNoCopy()); - for (size_t j = 0; j < replaceCount; j++) { - framebufferPatch.connectors[i+j] = replacementConnectors[j]; - connectorPatchFlags[i+j].bits.CPFIndex = true; - connectorPatchFlags[i+j].bits.CPFBusId = true; - connectorPatchFlags[i+j].bits.CPFPipe = true; - connectorPatchFlags[i+j].bits.CPFType = true; - connectorPatchFlags[i+j].bits.CPFFlags = true; - } - } - } - - snprintf(name, sizeof(name), "framebuffer-con%lu-index", i); - connectorPatchFlags[i].bits.CPFIndex |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].index); - snprintf(name, sizeof(name), "framebuffer-con%lu-busid", i); - connectorPatchFlags[i].bits.CPFBusId |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].busId); - snprintf(name, sizeof(name), "framebuffer-con%lu-pipe", i); - connectorPatchFlags[i].bits.CPFPipe |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].pipe); - snprintf(name, sizeof(name), "framebuffer-con%lu-type", i); - connectorPatchFlags[i].bits.CPFType |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].type); - snprintf(name, sizeof(name), "framebuffer-con%lu-flags", i); - connectorPatchFlags[i].bits.CPFFlags |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].flags.value); - - if (connectorPatchFlags[i].value != 0) - hasFramebufferPatch = true; - } - } - - size_t patchIndex = 0; - for (size_t i = 0; i < MaxFramebufferPatchCount; i++) { - char name[48]; - snprintf(name, sizeof(name), "framebuffer-patch%lu-enable", i); - // Missing status means no patches at all. - uint32_t framebufferPatchEnable = 0; - if (!WIOKit::getOSDataValue(igpu, name, framebufferPatchEnable)) - break; - - // False status means a temporarily disabled patch, skip for next one. - if (!framebufferPatchEnable) - continue; - - uint32_t framebufferId = 0; - size_t framebufferPatchCount = 0; - - snprintf(name, sizeof(name), "framebuffer-patch%lu-framebufferid", i); - bool passedFramebufferId = WIOKit::getOSDataValue(igpu, name, framebufferId); - snprintf(name, sizeof(name), "framebuffer-patch%lu-find", i); - auto framebufferPatchFind = OSDynamicCast(OSData, igpu->getProperty(name)); - snprintf(name, sizeof(name), "framebuffer-patch%lu-replace", i); - auto framebufferPatchReplace = OSDynamicCast(OSData, igpu->getProperty(name)); - snprintf(name, sizeof(name), "framebuffer-patch%lu-count", i); - WIOKit::getOSDataValue(igpu, name, framebufferPatchCount); - - if (!framebufferPatchFind || !framebufferPatchReplace) - continue; - - framebufferPatches[patchIndex].framebufferId = (passedFramebufferId ? framebufferId : currentFramebufferId); - framebufferPatches[patchIndex].find = framebufferPatchFind; - framebufferPatches[patchIndex].replace = framebufferPatchReplace; - framebufferPatches[patchIndex].count = (framebufferPatchCount ? framebufferPatchCount : 1); - - framebufferPatchFind->retain(); - framebufferPatchReplace->retain(); - - hasFramebufferPatch = true; - - patchIndex++; - } - - return hasFramebufferPatch; + bool hasFramebufferPatch = false; + + uint32_t framebufferPatchEnable = 0; + if (WIOKit::getOSDataValue(igpu, "framebuffer-patch-enable", framebufferPatchEnable) && framebufferPatchEnable) { + DBGLOG("igfx", "framebuffer-patch-enable %d", framebufferPatchEnable); + + // Note, the casts to uint32_t here and below are required due to device properties always injecting 32-bit types. + framebufferPatchFlags.bits.FPFFramebufferId = WIOKit::getOSDataValue(igpu, "framebuffer-framebufferid", framebufferPatch.framebufferId); + framebufferPatchFlags.bits.FPFFlags = WIOKit::getOSDataValue(igpu, "framebuffer-flags", framebufferPatch.flags.value); + framebufferPatchFlags.bits.FPFCamelliaVersion = WIOKit::getOSDataValue(igpu, "framebuffer-camellia", framebufferPatch.camelliaVersion); + framebufferPatchFlags.bits.FPFMobile = WIOKit::getOSDataValue(igpu, "framebuffer-mobile", framebufferPatch.fMobile); + framebufferPatchFlags.bits.FPFPipeCount = WIOKit::getOSDataValue(igpu, "framebuffer-pipecount", framebufferPatch.fPipeCount); + framebufferPatchFlags.bits.FPFPortCount = WIOKit::getOSDataValue(igpu, "framebuffer-portcount", framebufferPatch.fPortCount); + framebufferPatchFlags.bits.FPFFBMemoryCount = WIOKit::getOSDataValue(igpu, "framebuffer-memorycount", framebufferPatch.fFBMemoryCount); + framebufferPatchFlags.bits.FPFStolenMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-stolenmem", framebufferPatch.fStolenMemorySize); + framebufferPatchFlags.bits.FPFFramebufferMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-fbmem", framebufferPatch.fFramebufferMemorySize); + framebufferPatchFlags.bits.FPFUnifiedMemorySize = WIOKit::getOSDataValue(igpu, "framebuffer-unifiedmem", framebufferPatch.fUnifiedMemorySize); + framebufferPatchFlags.bits.FPFFramebufferCursorSize = WIOKit::getOSDataValue(igpu, "framebuffer-cursormem", fPatchCursorMemorySize); + + if (framebufferPatchFlags.value != 0) + hasFramebufferPatch = true; + + for (size_t i = 0; i < arrsize(framebufferPatch.connectors); i++) { + char name[48]; + snprintf(name, sizeof(name), "framebuffer-con%lu-enable", i); + uint32_t framebufferConnectorPatchEnable = 0; + if (!WIOKit::getOSDataValue(igpu, name, framebufferConnectorPatchEnable) || !framebufferConnectorPatchEnable) + continue; + + DBGLOG("igfx", "framebuffer-con%lu-enable %d", i, framebufferConnectorPatchEnable); + + snprintf(name, sizeof(name), "framebuffer-con%lu-%08x-alldata", i, currentFramebufferId); + auto allData = OSDynamicCast(OSData, igpu->getProperty(name)); + if (!allData) { + snprintf(name, sizeof(name), "framebuffer-con%lu-alldata", i); + allData = OSDynamicCast(OSData, igpu->getProperty(name)); + } + if (allData) { + auto allDataSize = allData->getLength(); + auto replaceCount = allDataSize / sizeof(framebufferPatch.connectors[0]); + if (0 == allDataSize % sizeof(framebufferPatch.connectors[0]) && i + replaceCount <= arrsize(framebufferPatch.connectors)) { + auto replacementConnectors = reinterpret_cast(allData->getBytesNoCopy()); + for (size_t j = 0; j < replaceCount; j++) { + framebufferPatch.connectors[i+j] = replacementConnectors[j]; + connectorPatchFlags[i+j].bits.CPFIndex = true; + connectorPatchFlags[i+j].bits.CPFBusId = true; + connectorPatchFlags[i+j].bits.CPFPipe = true; + connectorPatchFlags[i+j].bits.CPFType = true; + connectorPatchFlags[i+j].bits.CPFFlags = true; + } + } + } + + snprintf(name, sizeof(name), "framebuffer-con%lu-index", i); + connectorPatchFlags[i].bits.CPFIndex |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].index); + snprintf(name, sizeof(name), "framebuffer-con%lu-busid", i); + connectorPatchFlags[i].bits.CPFBusId |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].busId); + snprintf(name, sizeof(name), "framebuffer-con%lu-pipe", i); + connectorPatchFlags[i].bits.CPFPipe |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].pipe); + snprintf(name, sizeof(name), "framebuffer-con%lu-type", i); + connectorPatchFlags[i].bits.CPFType |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].type); + snprintf(name, sizeof(name), "framebuffer-con%lu-flags", i); + connectorPatchFlags[i].bits.CPFFlags |= WIOKit::getOSDataValue(igpu, name, framebufferPatch.connectors[i].flags.value); + + if (connectorPatchFlags[i].value != 0) + hasFramebufferPatch = true; + } + } + + size_t patchIndex = 0; + for (size_t i = 0; i < MaxFramebufferPatchCount; i++) { + char name[48]; + snprintf(name, sizeof(name), "framebuffer-patch%lu-enable", i); + // Missing status means no patches at all. + uint32_t framebufferPatchEnable = 0; + if (!WIOKit::getOSDataValue(igpu, name, framebufferPatchEnable)) + break; + + // False status means a temporarily disabled patch, skip for next one. + if (!framebufferPatchEnable) + continue; + + uint32_t framebufferId = 0; + size_t framebufferPatchCount = 0; + + snprintf(name, sizeof(name), "framebuffer-patch%lu-framebufferid", i); + bool passedFramebufferId = WIOKit::getOSDataValue(igpu, name, framebufferId); + snprintf(name, sizeof(name), "framebuffer-patch%lu-find", i); + auto framebufferPatchFind = OSDynamicCast(OSData, igpu->getProperty(name)); + snprintf(name, sizeof(name), "framebuffer-patch%lu-replace", i); + auto framebufferPatchReplace = OSDynamicCast(OSData, igpu->getProperty(name)); + snprintf(name, sizeof(name), "framebuffer-patch%lu-count", i); + WIOKit::getOSDataValue(igpu, name, framebufferPatchCount); + + if (!framebufferPatchFind || !framebufferPatchReplace) + continue; + + framebufferPatches[patchIndex].framebufferId = (passedFramebufferId ? framebufferId : currentFramebufferId); + framebufferPatches[patchIndex].find = framebufferPatchFind; + framebufferPatches[patchIndex].replace = framebufferPatchReplace; + framebufferPatches[patchIndex].count = (framebufferPatchCount ? framebufferPatchCount : 1); + + framebufferPatchFind->retain(); + framebufferPatchReplace->retain(); + + hasFramebufferPatch = true; + + patchIndex++; + } + + return hasFramebufferPatch; } uint8_t *IGFX::findFramebufferId(uint32_t framebufferId, uint8_t *startingAddress, size_t maxSize) { - uint32_t *startAddress = reinterpret_cast(startingAddress); - uint32_t *endAddress = reinterpret_cast(startingAddress + maxSize); - while (startAddress < endAddress) { - if (*startAddress == framebufferId) - return reinterpret_cast(startAddress); - startAddress++; - } - - return nullptr; + uint32_t *startAddress = reinterpret_cast(startingAddress); + uint32_t *endAddress = reinterpret_cast(startingAddress + maxSize); + while (startAddress < endAddress) { + if (*startAddress == framebufferId) + return reinterpret_cast(startAddress); + startAddress++; + } + + return nullptr; } #ifdef DEBUG size_t IGFX::calculatePlatformListSize(size_t maxSize) { - // sanity check maxSize - if (maxSize < sizeof(uint32_t)*2) - return maxSize; - // ig-platform-id table ends with 0xFFFFF, but to avoid false positive - // look for FFFFFFFF 00000000 - // and Sandy Bridge is special, ending in 00000000 000c0c0c - uint8_t * startingAddress = reinterpret_cast(gPlatformInformationList); - uint32_t *startAddress = reinterpret_cast(startingAddress); - uint32_t *endAddress = reinterpret_cast(startingAddress + maxSize - sizeof(uint32_t)); - while (startAddress < endAddress) { - if ((!gPlatformListIsSNB && 0xffffffff == startAddress[0] && 0 == startAddress[1]) || - (gPlatformListIsSNB && 0 == startAddress[0] && 0x0c0c0c00 == startAddress[1])) - return reinterpret_cast(startAddress) - startingAddress + sizeof(uint32_t)*2; - startAddress++; - } - - return maxSize; // in case of no termination, just return maxSize + // sanity check maxSize + if (maxSize < sizeof(uint32_t)*2) + return maxSize; + // ig-platform-id table ends with 0xFFFFF, but to avoid false positive + // look for FFFFFFFF 00000000 + // and Sandy Bridge is special, ending in 00000000 000c0c0c + uint8_t * startingAddress = reinterpret_cast(gPlatformInformationList); + uint32_t *startAddress = reinterpret_cast(startingAddress); + uint32_t *endAddress = reinterpret_cast(startingAddress + maxSize - sizeof(uint32_t)); + while (startAddress < endAddress) { + if ((!gPlatformListIsSNB && 0xffffffff == startAddress[0] && 0 == startAddress[1]) || + (gPlatformListIsSNB && 0 == startAddress[0] && 0x0c0c0c00 == startAddress[1])) + return reinterpret_cast(startAddress) - startingAddress + sizeof(uint32_t)*2; + startAddress++; + } + + return maxSize; // in case of no termination, just return maxSize } void IGFX::writePlatformListData(const char *subKeyName) { - auto entry = IORegistryEntry::fromPath("IOService:/IOResources/WhateverGreen"); - if (entry) { - entry->setProperty(subKeyName, gPlatformInformationList, static_cast(calculatePlatformListSize(PAGE_SIZE))); - entry->release(); - } + auto entry = IORegistryEntry::fromPath("IOService:/IOResources/WhateverGreen"); + if (entry) { + entry->setProperty(subKeyName, gPlatformInformationList, static_cast(calculatePlatformListSize(PAGE_SIZE))); + entry->release(); + } } #endif bool IGFX::applyPatch(const KernelPatcher::LookupPatch &patch, uint8_t *startingAddress, size_t maxSize) { - bool r = false; - size_t i = 0, patchCount = 0; - uint8_t *startAddress = startingAddress; - uint8_t *endAddress = startingAddress + maxSize - patch.size; - - if (startAddress < framebufferStart) - startAddress = framebufferStart; - if (endAddress > framebufferStart + framebufferSize) - endAddress = framebufferStart + framebufferSize; - - while (startAddress < endAddress) { - for (i = 0; i < patch.size; i++) { - if (startAddress[i] != patch.find[i]) - break; - } - if (i == patch.size) { - for (i = 0; i < patch.size; i++) - startAddress[i] = patch.replace[i]; - - r = true; - - if (++patchCount >= patch.count) - break; - - startAddress += patch.size; - continue; - } - - startAddress++; - } - - return r; + bool r = false; + size_t i = 0, patchCount = 0; + uint8_t *startAddress = startingAddress; + uint8_t *endAddress = startingAddress + maxSize - patch.size; + + if (startAddress < framebufferStart) + startAddress = framebufferStart; + if (endAddress > framebufferStart + framebufferSize) + endAddress = framebufferStart + framebufferSize; + + while (startAddress < endAddress) { + for (i = 0; i < patch.size; i++) { + if (startAddress[i] != patch.find[i]) + break; + } + if (i == patch.size) { + for (i = 0; i < patch.size; i++) + startAddress[i] = patch.replace[i]; + + r = true; + + if (++patchCount >= patch.count) + break; + + startAddress += patch.size; + continue; + } + + startAddress++; + } + + return r; } template <> bool IGFX::applyPlatformInformationListPatch(uint32_t framebufferId, FramebufferSNB *platformInformationList) { - bool framebufferFound = false; + bool framebufferFound = false; - for (size_t i = 0; i < SandyPlatformNum; i++) { - if (sandyPlatformId[i] == framebufferId) { - if (framebufferPatchFlags.bits.FPFMobile) - platformInformationList[i].fMobile = framebufferPatch.fMobile; + for (size_t i = 0; i < SandyPlatformNum; i++) { + if (sandyPlatformId[i] == framebufferId) { + if (framebufferPatchFlags.bits.FPFMobile) + platformInformationList[i].fMobile = framebufferPatch.fMobile; - if (framebufferPatchFlags.bits.FPFPipeCount) - platformInformationList[i].fPipeCount = framebufferPatch.fPipeCount; + if (framebufferPatchFlags.bits.FPFPipeCount) + platformInformationList[i].fPipeCount = framebufferPatch.fPipeCount; - if (framebufferPatchFlags.bits.FPFPortCount) - platformInformationList[i].fPortCount = framebufferPatch.fPortCount; + if (framebufferPatchFlags.bits.FPFPortCount) + platformInformationList[i].fPortCount = framebufferPatch.fPortCount; - if (framebufferPatchFlags.bits.FPFFBMemoryCount) - platformInformationList[i].fFBMemoryCount = framebufferPatch.fFBMemoryCount; + if (framebufferPatchFlags.bits.FPFFBMemoryCount) + platformInformationList[i].fFBMemoryCount = framebufferPatch.fFBMemoryCount; - for (size_t j = 0; j < arrsize(platformInformationList[i].connectors); j++) { - if (connectorPatchFlags[j].bits.CPFIndex) - platformInformationList[i].connectors[j].index = framebufferPatch.connectors[j].index; + for (size_t j = 0; j < arrsize(platformInformationList[i].connectors); j++) { + if (connectorPatchFlags[j].bits.CPFIndex) + platformInformationList[i].connectors[j].index = framebufferPatch.connectors[j].index; - if (connectorPatchFlags[j].bits.CPFBusId) - platformInformationList[i].connectors[j].busId = framebufferPatch.connectors[j].busId; + if (connectorPatchFlags[j].bits.CPFBusId) + platformInformationList[i].connectors[j].busId = framebufferPatch.connectors[j].busId; - if (connectorPatchFlags[j].bits.CPFPipe) - platformInformationList[i].connectors[j].pipe = framebufferPatch.connectors[j].pipe; + if (connectorPatchFlags[j].bits.CPFPipe) + platformInformationList[i].connectors[j].pipe = framebufferPatch.connectors[j].pipe; - if (connectorPatchFlags[j].bits.CPFType) - platformInformationList[i].connectors[j].type = framebufferPatch.connectors[j].type; + if (connectorPatchFlags[j].bits.CPFType) + platformInformationList[i].connectors[j].type = framebufferPatch.connectors[j].type; - if (connectorPatchFlags[j].bits.CPFFlags) - platformInformationList[i].connectors[j].flags = framebufferPatch.connectors[j].flags; + if (connectorPatchFlags[j].bits.CPFFlags) + platformInformationList[i].connectors[j].flags = framebufferPatch.connectors[j].flags; - if (connectorPatchFlags[j].value) { - DBGLOG("igfx", "patching framebufferId 0x%08X connector [%d] busId: 0x%02X, pipe: %u, type: 0x%08X, flags: 0x%08X", framebufferId, platformInformationList[i].connectors[j].index, platformInformationList[i].connectors[j].busId, platformInformationList[i].connectors[j].pipe, platformInformationList[i].connectors[j].type, platformInformationList[i].connectors[j].flags.value); + if (connectorPatchFlags[j].value) { + DBGLOG("igfx", "patching framebufferId 0x%08X connector [%d] busId: 0x%02X, pipe: %u, type: 0x%08X, flags: 0x%08X", framebufferId, platformInformationList[i].connectors[j].index, platformInformationList[i].connectors[j].busId, platformInformationList[i].connectors[j].pipe, platformInformationList[i].connectors[j].type, platformInformationList[i].connectors[j].flags.value); - framebufferFound = true; - } - } + framebufferFound = true; + } + } - if (framebufferPatchFlags.value) { - DBGLOG("igfx", "patching framebufferId 0x%08X", framebufferId); - DBGLOG("igfx", "mobile: 0x%08X", platformInformationList[i].fMobile); - DBGLOG("igfx", "pipeCount: %u", platformInformationList[i].fPipeCount); - DBGLOG("igfx", "portCount: %u", platformInformationList[i].fPortCount); - DBGLOG("igfx", "fbMemoryCount: %u", platformInformationList[i].fFBMemoryCount); + if (framebufferPatchFlags.value) { + DBGLOG("igfx", "patching framebufferId 0x%08X", framebufferId); + DBGLOG("igfx", "mobile: 0x%08X", platformInformationList[i].fMobile); + DBGLOG("igfx", "pipeCount: %u", platformInformationList[i].fPipeCount); + DBGLOG("igfx", "portCount: %u", platformInformationList[i].fPortCount); + DBGLOG("igfx", "fbMemoryCount: %u", platformInformationList[i].fFBMemoryCount); - framebufferFound = true; - } - } - } + framebufferFound = true; + } + } + } - return framebufferFound; + return framebufferFound; } // Sandy and Ivy have no flags @@ -1210,250 +1233,250 @@ void IGFX::applyPlatformInformationPatchEx(FramebufferIVB *frame) {} template <> void IGFX::applyPlatformInformationPatchEx(FramebufferHSW *frame) { - // fCursorMemorySize is Haswell specific - if (framebufferPatchFlags.bits.FPFFramebufferCursorSize) { - frame->fCursorMemorySize = fPatchCursorMemorySize; - DBGLOG("igfx", "fCursorMemorySize: 0x%08X", frame->fCursorMemorySize); - } + // fCursorMemorySize is Haswell specific + if (framebufferPatchFlags.bits.FPFFramebufferCursorSize) { + frame->fCursorMemorySize = fPatchCursorMemorySize; + DBGLOG("igfx", "fCursorMemorySize: 0x%08X", frame->fCursorMemorySize); + } - if (framebufferPatchFlags.bits.FPFFlags) - frame->flags.value = framebufferPatch.flags.value; + if (framebufferPatchFlags.bits.FPFFlags) + frame->flags.value = framebufferPatch.flags.value; - if (framebufferPatchFlags.bits.FPFCamelliaVersion) - frame->camelliaVersion = framebufferPatch.camelliaVersion; + if (framebufferPatchFlags.bits.FPFCamelliaVersion) + frame->camelliaVersion = framebufferPatch.camelliaVersion; } template void IGFX::applyPlatformInformationPatchEx(T *frame) { - if (framebufferPatchFlags.bits.FPFFlags) - frame->flags.value = framebufferPatch.flags.value; + if (framebufferPatchFlags.bits.FPFFlags) + frame->flags.value = framebufferPatch.flags.value; - if (framebufferPatchFlags.bits.FPFCamelliaVersion) - frame->camelliaVersion = framebufferPatch.camelliaVersion; + if (framebufferPatchFlags.bits.FPFCamelliaVersion) + frame->camelliaVersion = framebufferPatch.camelliaVersion; } template bool IGFX::applyPlatformInformationListPatch(uint32_t framebufferId, T *platformInformationList) { - auto frame = reinterpret_cast(findFramebufferId(framebufferId, reinterpret_cast(platformInformationList), PAGE_SIZE)); - if (!frame) - return false; + auto frame = reinterpret_cast(findFramebufferId(framebufferId, reinterpret_cast(platformInformationList), PAGE_SIZE)); + if (!frame) + return false; - bool r = false; + bool r = false; - if (framebufferPatchFlags.bits.FPFMobile) - frame->fMobile = framebufferPatch.fMobile; + if (framebufferPatchFlags.bits.FPFMobile) + frame->fMobile = framebufferPatch.fMobile; - if (framebufferPatchFlags.bits.FPFPipeCount) - frame->fPipeCount = framebufferPatch.fPipeCount; + if (framebufferPatchFlags.bits.FPFPipeCount) + frame->fPipeCount = framebufferPatch.fPipeCount; - if (framebufferPatchFlags.bits.FPFPortCount) - frame->fPortCount = framebufferPatch.fPortCount; + if (framebufferPatchFlags.bits.FPFPortCount) + frame->fPortCount = framebufferPatch.fPortCount; - if (framebufferPatchFlags.bits.FPFFBMemoryCount) - frame->fFBMemoryCount = framebufferPatch.fFBMemoryCount; + if (framebufferPatchFlags.bits.FPFFBMemoryCount) + frame->fFBMemoryCount = framebufferPatch.fFBMemoryCount; - if (framebufferPatchFlags.bits.FPFStolenMemorySize) - frame->fStolenMemorySize = framebufferPatch.fStolenMemorySize; + if (framebufferPatchFlags.bits.FPFStolenMemorySize) + frame->fStolenMemorySize = framebufferPatch.fStolenMemorySize; - if (framebufferPatchFlags.bits.FPFFramebufferMemorySize) - frame->fFramebufferMemorySize = framebufferPatch.fFramebufferMemorySize; + if (framebufferPatchFlags.bits.FPFFramebufferMemorySize) + frame->fFramebufferMemorySize = framebufferPatch.fFramebufferMemorySize; - if (framebufferPatchFlags.bits.FPFUnifiedMemorySize) - frame->fUnifiedMemorySize = framebufferPatch.fUnifiedMemorySize; + if (framebufferPatchFlags.bits.FPFUnifiedMemorySize) + frame->fUnifiedMemorySize = framebufferPatch.fUnifiedMemorySize; - if (framebufferPatchFlags.value) { - DBGLOG("igfx", "patching framebufferId 0x%08X", frame->framebufferId); - DBGLOG("igfx", "mobile: 0x%08X", frame->fMobile); - DBGLOG("igfx", "pipeCount: %u", frame->fPipeCount); - DBGLOG("igfx", "portCount: %u", frame->fPortCount); - DBGLOG("igfx", "fbMemoryCount: %u", frame->fFBMemoryCount); - DBGLOG("igfx", "stolenMemorySize: 0x%08X", frame->fStolenMemorySize); - DBGLOG("igfx", "framebufferMemorySize: 0x%08X", frame->fFramebufferMemorySize); - DBGLOG("igfx", "unifiedMemorySize: 0x%08X", frame->fUnifiedMemorySize); + if (framebufferPatchFlags.value) { + DBGLOG("igfx", "patching framebufferId 0x%08X", frame->framebufferId); + DBGLOG("igfx", "mobile: 0x%08X", frame->fMobile); + DBGLOG("igfx", "pipeCount: %u", frame->fPipeCount); + DBGLOG("igfx", "portCount: %u", frame->fPortCount); + DBGLOG("igfx", "fbMemoryCount: %u", frame->fFBMemoryCount); + DBGLOG("igfx", "stolenMemorySize: 0x%08X", frame->fStolenMemorySize); + DBGLOG("igfx", "framebufferMemorySize: 0x%08X", frame->fFramebufferMemorySize); + DBGLOG("igfx", "unifiedMemorySize: 0x%08X", frame->fUnifiedMemorySize); - r = true; - } + r = true; + } - applyPlatformInformationPatchEx(frame); + applyPlatformInformationPatchEx(frame); - for (size_t j = 0; j < arrsize(frame->connectors); j++) { - if (connectorPatchFlags[j].bits.CPFIndex) - frame->connectors[j].index = framebufferPatch.connectors[j].index; + for (size_t j = 0; j < arrsize(frame->connectors); j++) { + if (connectorPatchFlags[j].bits.CPFIndex) + frame->connectors[j].index = framebufferPatch.connectors[j].index; - if (connectorPatchFlags[j].bits.CPFBusId) - frame->connectors[j].busId = framebufferPatch.connectors[j].busId; + if (connectorPatchFlags[j].bits.CPFBusId) + frame->connectors[j].busId = framebufferPatch.connectors[j].busId; - if (connectorPatchFlags[j].bits.CPFPipe) - frame->connectors[j].pipe = framebufferPatch.connectors[j].pipe; + if (connectorPatchFlags[j].bits.CPFPipe) + frame->connectors[j].pipe = framebufferPatch.connectors[j].pipe; - if (connectorPatchFlags[j].bits.CPFType) - frame->connectors[j].type = framebufferPatch.connectors[j].type; + if (connectorPatchFlags[j].bits.CPFType) + frame->connectors[j].type = framebufferPatch.connectors[j].type; - if (connectorPatchFlags[j].bits.CPFFlags) - frame->connectors[j].flags = framebufferPatch.connectors[j].flags; + if (connectorPatchFlags[j].bits.CPFFlags) + frame->connectors[j].flags = framebufferPatch.connectors[j].flags; - if (connectorPatchFlags[j].value) { - DBGLOG("igfx", "patching framebufferId 0x%08X connector [%d] busId: 0x%02X, pipe: %u, type: 0x%08X, flags: 0x%08X", frame->framebufferId, frame->connectors[j].index, frame->connectors[j].busId, frame->connectors[j].pipe, frame->connectors[j].type, frame->connectors[j].flags.value); + if (connectorPatchFlags[j].value) { + DBGLOG("igfx", "patching framebufferId 0x%08X connector [%d] busId: 0x%02X, pipe: %u, type: 0x%08X, flags: 0x%08X", frame->framebufferId, frame->connectors[j].index, frame->connectors[j].busId, frame->connectors[j].pipe, frame->connectors[j].type, frame->connectors[j].flags.value); - r = true; - } - } + r = true; + } + } - return r; + return r; } template <> bool IGFX::applyDPtoHDMIPatch(uint32_t framebufferId, FramebufferSNB *platformInformationList) { - bool found = false; - - for (size_t i = 0; i < SandyPlatformNum; i++) { - if (sandyPlatformId[i] == framebufferId) { - for (size_t j = 0; j < arrsize(platformInformationList[i].connectors); j++) { - DBGLOG("igfx", "snb connector [%lu] busId: 0x%02X, pipe: %d, type: 0x%08X, flags: 0x%08X", j, platformInformationList[i].connectors[j].busId, platformInformationList[i].connectors[j].pipe, - platformInformationList[i].connectors[j].type, platformInformationList[i].connectors[j].flags); - - if (platformInformationList[i].connectors[j].type == ConnectorDP) { - platformInformationList[i].connectors[j].type = ConnectorHDMI; - DBGLOG("igfx", "replaced snb connector %lu type from DP to HDMI", j); - found = true; - } - } - } - } - - return found; + bool found = false; + + for (size_t i = 0; i < SandyPlatformNum; i++) { + if (sandyPlatformId[i] == framebufferId) { + for (size_t j = 0; j < arrsize(platformInformationList[i].connectors); j++) { + DBGLOG("igfx", "snb connector [%lu] busId: 0x%02X, pipe: %d, type: 0x%08X, flags: 0x%08X", j, platformInformationList[i].connectors[j].busId, platformInformationList[i].connectors[j].pipe, + platformInformationList[i].connectors[j].type, platformInformationList[i].connectors[j].flags); + + if (platformInformationList[i].connectors[j].type == ConnectorDP) { + platformInformationList[i].connectors[j].type = ConnectorHDMI; + DBGLOG("igfx", "replaced snb connector %lu type from DP to HDMI", j); + found = true; + } + } + } + } + + return found; } template bool IGFX::applyDPtoHDMIPatch(uint32_t framebufferId, T *platformInformationList) { - auto frame = reinterpret_cast(findFramebufferId(framebufferId, reinterpret_cast(platformInformationList), PAGE_SIZE)); - if (!frame) - return false; - - bool found = false; - for (size_t i = 0; i < arrsize(frame->connectors); i++) { - DBGLOG("igfx", "connector [%lu] busId: 0x%02X, pipe: %d, type: 0x%08X, flags: 0x%08X", i, platformInformationList[i].connectors[i].busId, platformInformationList[i].connectors[i].pipe, - platformInformationList[i].connectors[i].type, platformInformationList[i].connectors[i].flags); - - if (frame->connectors[i].type == ConnectorDP) { - frame->connectors[i].type = ConnectorHDMI; - DBGLOG("igfx", "replaced connector %lu type from DP to HDMI", i); - found = true; - } - } - - return found; + auto frame = reinterpret_cast(findFramebufferId(framebufferId, reinterpret_cast(platformInformationList), PAGE_SIZE)); + if (!frame) + return false; + + bool found = false; + for (size_t i = 0; i < arrsize(frame->connectors); i++) { + DBGLOG("igfx", "connector [%lu] busId: 0x%02X, pipe: %d, type: 0x%08X, flags: 0x%08X", i, platformInformationList[i].connectors[i].busId, platformInformationList[i].connectors[i].pipe, + platformInformationList[i].connectors[i].type, platformInformationList[i].connectors[i].flags); + + if (frame->connectors[i].type == ConnectorDP) { + frame->connectors[i].type = ConnectorHDMI; + DBGLOG("igfx", "replaced connector %lu type from DP to HDMI", i); + found = true; + } + } + + return found; } void IGFX::applyFramebufferPatches() { - uint32_t framebufferId = framebufferPatch.framebufferId; - - // Not tested prior to 10.10.5, and definitely different on 10.9.5 at least. - if (getKernelVersion() >= KernelVersion::Yosemite) { - bool success = false; - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Skylake || cpuGeneration == CPUInfo::CpuGeneration::KabyLake || - (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake && static_cast(gPlatformInformationList)->framebufferId == 0x591E0000)) - //FIXME: write this in a nicer way (coffee pretending to be Kaby, detecting via first kaby frame) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::CannonLake) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::IceLake) { - // FIXME: Need to address possible circumstance of both ICL kexts loaded at the same time - if (callbackIGFX->currentFramebuffer->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (callbackIGFX->currentFramebufferOpt->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) - success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); - } - - if (success) - DBGLOG("igfx", "patching framebufferId 0x%08X successful", framebufferId); - else - DBGLOG("igfx", "patching framebufferId 0x%08X failed", framebufferId); - } - - uint8_t *platformInformationAddress = findFramebufferId(framebufferId, static_cast(gPlatformInformationList), PAGE_SIZE); - if (platformInformationAddress) { - for (size_t i = 0; i < MaxFramebufferPatchCount; i++) { - if (!framebufferPatches[i].find || !framebufferPatches[i].replace) - continue; - - if (framebufferPatches[i].framebufferId != framebufferId) { - framebufferId = framebufferPatches[i].framebufferId; - platformInformationAddress = findFramebufferId(framebufferId, static_cast(gPlatformInformationList), PAGE_SIZE); - } - - if (!platformInformationAddress) { - DBGLOG("igfx", "patch %lu framebufferId 0x%08X not found", i, framebufferId); - continue; - } - - if (framebufferPatches[i].find->getLength() != framebufferPatches[i].replace->getLength()) { - DBGLOG("igfx", "patch %lu framebufferId 0x%08X length mistmatch", i, framebufferId); - continue; - } - - KernelPatcher::LookupPatch patch {}; - patch.kext = currentFramebuffer; - patch.find = static_cast(framebufferPatches[i].find->getBytesNoCopy()); - patch.replace = static_cast(framebufferPatches[i].replace->getBytesNoCopy()); - patch.size = framebufferPatches[i].find->getLength(); - patch.count = framebufferPatches[i].count; - - if (applyPatch(patch, platformInformationAddress, PAGE_SIZE)) - DBGLOG("igfx", "patch %lu framebufferId 0x%08X successful", i, framebufferId); - else - DBGLOG("igfx", "patch %lu framebufferId 0x%08X failed", i, framebufferId); - - framebufferPatches[i].find->release(); - framebufferPatches[i].find = nullptr; - framebufferPatches[i].replace->release(); - framebufferPatches[i].replace = nullptr; - } - } + uint32_t framebufferId = framebufferPatch.framebufferId; + + // Not tested prior to 10.10.5, and definitely different on 10.9.5 at least. + if (getKernelVersion() >= KernelVersion::Yosemite) { + bool success = false; + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Skylake || cpuGeneration == CPUInfo::CpuGeneration::KabyLake || + (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake && static_cast(gPlatformInformationList)->framebufferId == 0x591E0000)) + //FIXME: write this in a nicer way (coffee pretending to be Kaby, detecting via first kaby frame) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::CannonLake) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::IceLake) { + // FIXME: Need to address possible circumstance of both ICL kexts loaded at the same time + if (callbackIGFX->currentFramebuffer->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (callbackIGFX->currentFramebufferOpt->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) + success = applyPlatformInformationListPatch(framebufferId, static_cast(gPlatformInformationList)); + } + + if (success) + DBGLOG("igfx", "patching framebufferId 0x%08X successful", framebufferId); + else + DBGLOG("igfx", "patching framebufferId 0x%08X failed", framebufferId); + } + + uint8_t *platformInformationAddress = findFramebufferId(framebufferId, static_cast(gPlatformInformationList), PAGE_SIZE); + if (platformInformationAddress) { + for (size_t i = 0; i < MaxFramebufferPatchCount; i++) { + if (!framebufferPatches[i].find || !framebufferPatches[i].replace) + continue; + + if (framebufferPatches[i].framebufferId != framebufferId) { + framebufferId = framebufferPatches[i].framebufferId; + platformInformationAddress = findFramebufferId(framebufferId, static_cast(gPlatformInformationList), PAGE_SIZE); + } + + if (!platformInformationAddress) { + DBGLOG("igfx", "patch %lu framebufferId 0x%08X not found", i, framebufferId); + continue; + } + + if (framebufferPatches[i].find->getLength() != framebufferPatches[i].replace->getLength()) { + DBGLOG("igfx", "patch %lu framebufferId 0x%08X length mistmatch", i, framebufferId); + continue; + } + + KernelPatcher::LookupPatch patch {}; + patch.kext = currentFramebuffer; + patch.find = static_cast(framebufferPatches[i].find->getBytesNoCopy()); + patch.replace = static_cast(framebufferPatches[i].replace->getBytesNoCopy()); + patch.size = framebufferPatches[i].find->getLength(); + patch.count = framebufferPatches[i].count; + + if (applyPatch(patch, platformInformationAddress, PAGE_SIZE)) + DBGLOG("igfx", "patch %lu framebufferId 0x%08X successful", i, framebufferId); + else + DBGLOG("igfx", "patch %lu framebufferId 0x%08X failed", i, framebufferId); + + framebufferPatches[i].find->release(); + framebufferPatches[i].find = nullptr; + framebufferPatches[i].replace->release(); + framebufferPatches[i].replace = nullptr; + } + } } void IGFX::applyHdmiAutopatch() { - uint32_t framebufferId = framebufferPatch.framebufferId; - - DBGLOG("igfx", "applyHdmiAutopatch framebufferId %X cpugen %X", framebufferId, cpuGeneration); - - bool success = false; - if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::Skylake || cpuGeneration == CPUInfo::CpuGeneration::KabyLake || - (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake && static_cast(gPlatformInformationList)->framebufferId == 0x591E0000)) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::CannonLake) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (cpuGeneration == CPUInfo::CpuGeneration::IceLake) { - // FIXME: Need to address possible circumstance of both ICL kexts loaded at the same time - if (callbackIGFX->currentFramebuffer->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - else if (callbackIGFX->currentFramebufferOpt->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) - success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); - } - - if (success) - DBGLOG("igfx", "hdmi patching framebufferId 0x%08X successful", framebufferId); - else - DBGLOG("igfx", "hdmi patching framebufferId 0x%08X failed", framebufferId); + uint32_t framebufferId = framebufferPatch.framebufferId; + + DBGLOG("igfx", "applyHdmiAutopatch framebufferId %X cpugen %X", framebufferId, cpuGeneration); + + bool success = false; + if (cpuGeneration == CPUInfo::CpuGeneration::SandyBridge) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::IvyBridge) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Haswell) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Broadwell) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::Skylake || cpuGeneration == CPUInfo::CpuGeneration::KabyLake || + (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake && static_cast(gPlatformInformationList)->framebufferId == 0x591E0000)) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::CoffeeLake) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::CannonLake) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (cpuGeneration == CPUInfo::CpuGeneration::IceLake) { + // FIXME: Need to address possible circumstance of both ICL kexts loaded at the same time + if (callbackIGFX->currentFramebuffer->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + else if (callbackIGFX->currentFramebufferOpt->loadIndex == KernelPatcher::KextInfo::SysFlags::Loaded) + success = applyDPtoHDMIPatch(framebufferId, static_cast(gPlatformInformationList)); + } + + if (success) + DBGLOG("igfx", "hdmi patching framebufferId 0x%08X successful", framebufferId); + else + DBGLOG("igfx", "hdmi patching framebufferId 0x%08X failed", framebufferId); } diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index ac088820..524bf13f 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -17,632 +17,632 @@ class IGFX { public: - void init(); - void deinit(); - - /** - * Property patching routine - * - * @param patcher KernelPatcher instance - * @param info device info - */ - void processKernel(KernelPatcher &patcher, DeviceInfo *info); - - /** - * Patch kext if needed and prepare other patches - * - * @param patcher KernelPatcher instance - * @param index kinfo handle - * @param address kinfo load address - * @param size kinfo memory size - * - * @return true if patched anything - */ - bool processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); + void init(); + void deinit(); + + /** + * Property patching routine + * + * @param patcher KernelPatcher instance + * @param info device info + */ + void processKernel(KernelPatcher &patcher, DeviceInfo *info); + + /** + * Patch kext if needed and prepare other patches + * + * @param patcher KernelPatcher instance + * @param index kinfo handle + * @param address kinfo load address + * @param size kinfo memory size + * + * @return true if patched anything + */ + bool processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); private: - /** - * Framebuffer patch flags - */ - union FramebufferPatchFlags { - struct FramebufferPatchFlagBits { - uint8_t FPFFramebufferId :1; - uint8_t FPFModelNameAddr :1; - uint8_t FPFMobile :1; - uint8_t FPFPipeCount :1; - uint8_t FPFPortCount :1; - uint8_t FPFFBMemoryCount :1; - uint8_t FPFStolenMemorySize :1; - uint8_t FPFFramebufferMemorySize :1; - uint8_t FPFUnifiedMemorySize :1; - uint8_t FPFFramebufferCursorSize :1; // Haswell only - uint8_t FPFFlags :1; - uint8_t FPFBTTableOffsetIndexSlice :1; - uint8_t FPFBTTableOffsetIndexNormal :1; - uint8_t FPFBTTableOffsetIndexHDMI :1; - uint8_t FPFCamelliaVersion :1; - uint8_t FPFNumTransactionsThreshold :1; - uint8_t FPFVideoTurboFreq :1; - uint8_t FPFBTTArraySliceAddr :1; - uint8_t FPFBTTArrayNormalAddr :1; - uint8_t FPFBTTArrayHDMIAddr :1; - uint8_t FPFSliceCount :1; - uint8_t FPFEuCount :1; - } bits; - uint32_t value; - }; - - /** - * Connector patch flags - */ - union ConnectorPatchFlags { - struct ConnectorPatchFlagBits { - uint8_t CPFIndex :1; - uint8_t CPFBusId :1; - uint8_t CPFPipe :1; - uint8_t CPFType :1; - uint8_t CPFFlags :1; - } bits; - uint32_t value; - }; - - /** - * Framebuffer find / replace patch struct - */ - struct FramebufferPatch { - uint32_t framebufferId; - OSData *find; - OSData *replace; - size_t count; - }; - - /** - * Framebuffer patching flags - */ - FramebufferPatchFlags framebufferPatchFlags {}; - - /** - * Connector patching flags - */ - ConnectorPatchFlags connectorPatchFlags[MaxFramebufferConnectorCount] {}; - - /** - * Framebuffer hard-code patch - */ - FramebufferCFL framebufferPatch {}; - - /** - * Patch value for fCursorMemorySize in Haswell framebuffer - * This member is not present in FramebufferCFL, hence its addition here. - */ - uint32_t fPatchCursorMemorySize; - - /** - * Maximum find / replace patches - */ - static constexpr size_t MaxFramebufferPatchCount = 10; - - /** - * Backlight registers - */ - static constexpr uint32_t BXT_BLC_PWM_CTL1 = 0xC8250; - static constexpr uint32_t BXT_BLC_PWM_FREQ1 = 0xC8254; - static constexpr uint32_t BXT_BLC_PWM_DUTY1 = 0xC8258; - - /** - * Number of SNB frames in a framebuffer kext - */ - static constexpr size_t SandyPlatformNum = 9; - - /** - * SNB frame ids in a framebuffer kext - */ - uint32_t sandyPlatformId[SandyPlatformNum] { - 0x00010000, - 0x00020000, - 0x00030010, - 0x00030030, - 0x00040000, - 0xFFFFFFFF, - 0xFFFFFFFF, - 0x00030020, - 0x00050000 - }; - - /** - * Framebuffer find / replace patches - */ - FramebufferPatch framebufferPatches[MaxFramebufferPatchCount] {}; - - /** - * Framebuffer list, imported from the framebuffer kext - */ - void *gPlatformInformationList {nullptr}; - - /** - * Framebuffer list is in Sandy Bridge format - */ - bool gPlatformListIsSNB {false}; - - /** - * Private self instance for callbacks - */ - static IGFX *callbackIGFX; - - /** - * Current graphics kext used for modification - */ - KernelPatcher::KextInfo *currentGraphics {nullptr}; - - /** - * Current framebuffer kext used for modification - */ - KernelPatcher::KextInfo *currentFramebuffer {nullptr}; - - /** - * Current framebuffer optional kext used for modification - */ - KernelPatcher::KextInfo *currentFramebufferOpt {nullptr}; - - /** - * Original PAVP session callback function used for PAVP command handling - */ - mach_vm_address_t orgPavpSessionCallback {}; - - /** - * Original AppleIntelFramebufferController::ComputeLaneCount function used for DP lane count calculation - */ - mach_vm_address_t orgComputeLaneCount {}; - - /** - * Original IOService::copyExistingServices function from the kernel - */ - mach_vm_address_t orgCopyExistingServices {}; - - /** - * Original IntelAccelerator::start function - */ - mach_vm_address_t orgAcceleratorStart {}; - - /** - * Original AppleIntelFramebufferController::getOSInformation function - */ - mach_vm_address_t orgGetOSInformation {}; - - /** - * Original IGHardwareGuC::loadGuCBinary function - */ - mach_vm_address_t orgLoadGuCBinary {}; - - /** - * Original IGScheduler4::loadFirmware function - */ - mach_vm_address_t orgLoadFirmware {}; - - /** - * Original IGHardwareGuC::initSchedControl function - */ - mach_vm_address_t orgInitSchedControl {}; - - /** - * Original IGSharedMappedBuffer::withOptions function - */ - mach_vm_address_t orgIgBufferWithOptions {}; - - /** - * Original IGMappedBuffer::getGPUVirtualAddress function - */ - mach_vm_address_t orgIgBufferGetGpuVirtualAddress {}; - - /** - * Original AppleIntelFramebufferController::ReadRegister32 function - */ - uint32_t (*orgCflReadRegister32)(void *, uint32_t) {nullptr}; - uint32_t (*orgKblReadRegister32)(void *, uint32_t) {nullptr}; - - /** - * Original AppleIntelFramebufferController::WriteRegister32 function - */ - void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; - void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; - - /** - * Original AppleIntelFramebufferController::ReadAUX function - */ - int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; - - /** - * Detected CPU generation of the host system - */ - CPUInfo::CpuGeneration cpuGeneration {}; - - /** - * Set to true if a black screen ComputeLaneCount patch is required - */ - bool blackScreenPatch {false}; - - /** - * Coffee Lake backlight patch configuration options - */ - enum class CoffeeBacklightPatch { - Auto = -1, - On = 1, - Off = 0 - }; - - /** - * Set to On if Coffee Lake backlight patch type required - * - boot-arg igfxcflbklt=0/1 forcibly turns patch on or off (override) - * - IGPU property enable-cfl-backlight-fix turns patch on - * - laptop with CFL CPU and CFL IGPU drivers turns patch on - */ - CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; - - /** - * Patch the maximum link rate in the DPCD buffer read from the built-in display - */ - bool maxLinkRatePatch {false}; - - /** - * Set to true if PAVP code should be disabled - */ - bool pavpDisablePatch {false}; - - /** - * Set to true if read descriptor patch should be enabled - */ - bool readDescriptorPatch {false}; - - /** - * Set to true to disable Metal support - */ - bool forceOpenGL {false}; - - /** - * Set to true if Sandy Bridge Gen6Accelerator should be renamed - */ - bool moderniseAccelerator {false}; - - /** - * Set to true to avoid incompatible GPU firmware loading - */ - bool avoidFirmwareLoading {false}; - - /** - * Requires framebuffer modifications - */ - bool applyFramebufferPatch {false}; - - /** - * Perform framebuffer dump to /AppleIntelFramebufferNUM - */ - bool dumpFramebufferToDisk {false}; - - /** - * Perform platform table dump to ioreg - */ - bool dumpPlatformTable {false}; - - /** - * Perform automatic DP -> HDMI replacement - */ - bool hdmiAutopatch {false}; - - /** - * Load GuC firmware - */ - bool loadGuCFirmware {false}; - - /** - * Currently loading GuC firmware - */ - bool performingFirmwareLoad {false}; - - /** - * Framebuffer address space start - */ - uint8_t *framebufferStart {nullptr}; - - /** - * Framebuffer address space size - */ - size_t framebufferSize {0}; - - /** - * Pointer to the original GuC firmware - */ - uint8_t *gKmGen9GuCBinary {nullptr}; - - /** - * Pointer to the original GuC firmware signature - */ - uint8_t *signaturePointer {nullptr}; - - /** - * Pointer to GuC firmware upload size assignment - */ - uint32_t *firmwareSizePointer {nullptr}; - - /** - * Dummy firmware buffer to store unused old firmware in - */ - uint8_t *dummyFirmwareBuffer {nullptr}; - - /** - * Actual firmware buffer we store our new firmware in - */ - uint8_t *realFirmwareBuffer {nullptr}; - - /** - * Actual intercepted binary sizes - */ - uint32_t realBinarySize {}; - - /** - * Store backlight level - */ - uint32_t backlightLevel {}; - - /** - * Fallback user-requested backlight frequency in case 0 was initially written to the register. - */ - static constexpr uint32_t FallbackTargetBacklightFrequency {120000}; - - /** - * User-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 at system start. - * Can be specified via max-backlight-freq property. - */ - uint32_t targetBacklightFrequency {}; - - /** - * User-requested pwm control value obtained from BXT_BLC_PWM_CTL1. - */ - uint32_t targetPwmControl {}; - - /** - * Driver-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 write attempt at system start. - */ - uint32_t driverBacklightFrequency {}; - - /** - * Represents the first 16 fields of the receiver capabilities defined in DPCD - * - * Main Reference: - * - DisplayPort Specification Version 1.2 - * - * Side Reference: - * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) - * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h - */ - struct DPCDCap16 // 16 bytes - { - // DPCD Revision (DP Config Version) - // Value: 0x10, 0x11, 0x12, 0x13, 0x14 - uint8_t revision; - - // Maximum Link Rate - // Value: 0x1E (HBR3) 8.1 Gbps - // 0x14 (HBR2) 5.4 Gbps - // 0x0C (3_24) 3.24 Gbps - // 0x0A (HBR) 2.7 Gbps - // 0x06 (RBR) 1.62 Gbps - // Reference: 0x0C is used by Apple internally. - uint8_t maxLinkRate; - - // Maximum Number of Lanes - // Value: 0x1 (HBR2) - // 0x2 (HBR) - // 0x4 (RBR) - // Side Notes: - // (1) Bit 7 is used to indicate whether the link is capable of enhanced framing. - // (2) Bit 6 is used to indicate whether TPS3 is supported. - uint8_t maxLaneCount; - - // Maximum Downspread - uint8_t maxDownspread; - - // Other fields omitted in this struct - // Detailed information can be found in the specification - uint8_t others[12]; - }; - - /** - * User-specified maximum link rate value in the DPCD buffer - * - * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display - */ + /** + * Framebuffer patch flags + */ + union FramebufferPatchFlags { + struct FramebufferPatchFlagBits { + uint8_t FPFFramebufferId :1; + uint8_t FPFModelNameAddr :1; + uint8_t FPFMobile :1; + uint8_t FPFPipeCount :1; + uint8_t FPFPortCount :1; + uint8_t FPFFBMemoryCount :1; + uint8_t FPFStolenMemorySize :1; + uint8_t FPFFramebufferMemorySize :1; + uint8_t FPFUnifiedMemorySize :1; + uint8_t FPFFramebufferCursorSize :1; // Haswell only + uint8_t FPFFlags :1; + uint8_t FPFBTTableOffsetIndexSlice :1; + uint8_t FPFBTTableOffsetIndexNormal :1; + uint8_t FPFBTTableOffsetIndexHDMI :1; + uint8_t FPFCamelliaVersion :1; + uint8_t FPFNumTransactionsThreshold :1; + uint8_t FPFVideoTurboFreq :1; + uint8_t FPFBTTArraySliceAddr :1; + uint8_t FPFBTTArrayNormalAddr :1; + uint8_t FPFBTTArrayHDMIAddr :1; + uint8_t FPFSliceCount :1; + uint8_t FPFEuCount :1; + } bits; + uint32_t value; + }; + + /** + * Connector patch flags + */ + union ConnectorPatchFlags { + struct ConnectorPatchFlagBits { + uint8_t CPFIndex :1; + uint8_t CPFBusId :1; + uint8_t CPFPipe :1; + uint8_t CPFType :1; + uint8_t CPFFlags :1; + } bits; + uint32_t value; + }; + + /** + * Framebuffer find / replace patch struct + */ + struct FramebufferPatch { + uint32_t framebufferId; + OSData *find; + OSData *replace; + size_t count; + }; + + /** + * Framebuffer patching flags + */ + FramebufferPatchFlags framebufferPatchFlags {}; + + /** + * Connector patching flags + */ + ConnectorPatchFlags connectorPatchFlags[MaxFramebufferConnectorCount] {}; + + /** + * Framebuffer hard-code patch + */ + FramebufferCFL framebufferPatch {}; + + /** + * Patch value for fCursorMemorySize in Haswell framebuffer + * This member is not present in FramebufferCFL, hence its addition here. + */ + uint32_t fPatchCursorMemorySize; + + /** + * Maximum find / replace patches + */ + static constexpr size_t MaxFramebufferPatchCount = 10; + + /** + * Backlight registers + */ + static constexpr uint32_t BXT_BLC_PWM_CTL1 = 0xC8250; + static constexpr uint32_t BXT_BLC_PWM_FREQ1 = 0xC8254; + static constexpr uint32_t BXT_BLC_PWM_DUTY1 = 0xC8258; + + /** + * Number of SNB frames in a framebuffer kext + */ + static constexpr size_t SandyPlatformNum = 9; + + /** + * SNB frame ids in a framebuffer kext + */ + uint32_t sandyPlatformId[SandyPlatformNum] { + 0x00010000, + 0x00020000, + 0x00030010, + 0x00030030, + 0x00040000, + 0xFFFFFFFF, + 0xFFFFFFFF, + 0x00030020, + 0x00050000 + }; + + /** + * Framebuffer find / replace patches + */ + FramebufferPatch framebufferPatches[MaxFramebufferPatchCount] {}; + + /** + * Framebuffer list, imported from the framebuffer kext + */ + void *gPlatformInformationList {nullptr}; + + /** + * Framebuffer list is in Sandy Bridge format + */ + bool gPlatformListIsSNB {false}; + + /** + * Private self instance for callbacks + */ + static IGFX *callbackIGFX; + + /** + * Current graphics kext used for modification + */ + KernelPatcher::KextInfo *currentGraphics {nullptr}; + + /** + * Current framebuffer kext used for modification + */ + KernelPatcher::KextInfo *currentFramebuffer {nullptr}; + + /** + * Current framebuffer optional kext used for modification + */ + KernelPatcher::KextInfo *currentFramebufferOpt {nullptr}; + + /** + * Original PAVP session callback function used for PAVP command handling + */ + mach_vm_address_t orgPavpSessionCallback {}; + + /** + * Original AppleIntelFramebufferController::ComputeLaneCount function used for DP lane count calculation + */ + mach_vm_address_t orgComputeLaneCount {}; + + /** + * Original IOService::copyExistingServices function from the kernel + */ + mach_vm_address_t orgCopyExistingServices {}; + + /** + * Original IntelAccelerator::start function + */ + mach_vm_address_t orgAcceleratorStart {}; + + /** + * Original AppleIntelFramebufferController::getOSInformation function + */ + mach_vm_address_t orgGetOSInformation {}; + + /** + * Original IGHardwareGuC::loadGuCBinary function + */ + mach_vm_address_t orgLoadGuCBinary {}; + + /** + * Original IGScheduler4::loadFirmware function + */ + mach_vm_address_t orgLoadFirmware {}; + + /** + * Original IGHardwareGuC::initSchedControl function + */ + mach_vm_address_t orgInitSchedControl {}; + + /** + * Original IGSharedMappedBuffer::withOptions function + */ + mach_vm_address_t orgIgBufferWithOptions {}; + + /** + * Original IGMappedBuffer::getGPUVirtualAddress function + */ + mach_vm_address_t orgIgBufferGetGpuVirtualAddress {}; + + /** + * Original AppleIntelFramebufferController::ReadRegister32 function + */ + uint32_t (*orgCflReadRegister32)(void *, uint32_t) {nullptr}; + uint32_t (*orgKblReadRegister32)(void *, uint32_t) {nullptr}; + + /** + * Original AppleIntelFramebufferController::WriteRegister32 function + */ + void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; + void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; + + /** + * Original AppleIntelFramebufferController::ReadAUX function + */ + int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; + + /** + * Detected CPU generation of the host system + */ + CPUInfo::CpuGeneration cpuGeneration {}; + + /** + * Set to true if a black screen ComputeLaneCount patch is required + */ + bool blackScreenPatch {false}; + + /** + * Coffee Lake backlight patch configuration options + */ + enum class CoffeeBacklightPatch { + Auto = -1, + On = 1, + Off = 0 + }; + + /** + * Set to On if Coffee Lake backlight patch type required + * - boot-arg igfxcflbklt=0/1 forcibly turns patch on or off (override) + * - IGPU property enable-cfl-backlight-fix turns patch on + * - laptop with CFL CPU and CFL IGPU drivers turns patch on + */ + CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; + + /** + * Patch the maximum link rate in the DPCD buffer read from the built-in display + */ + bool maxLinkRatePatch {false}; + + /** + * Set to true if PAVP code should be disabled + */ + bool pavpDisablePatch {false}; + + /** + * Set to true if read descriptor patch should be enabled + */ + bool readDescriptorPatch {false}; + + /** + * Set to true to disable Metal support + */ + bool forceOpenGL {false}; + + /** + * Set to true if Sandy Bridge Gen6Accelerator should be renamed + */ + bool moderniseAccelerator {false}; + + /** + * Set to true to avoid incompatible GPU firmware loading + */ + bool avoidFirmwareLoading {false}; + + /** + * Requires framebuffer modifications + */ + bool applyFramebufferPatch {false}; + + /** + * Perform framebuffer dump to /AppleIntelFramebufferNUM + */ + bool dumpFramebufferToDisk {false}; + + /** + * Perform platform table dump to ioreg + */ + bool dumpPlatformTable {false}; + + /** + * Perform automatic DP -> HDMI replacement + */ + bool hdmiAutopatch {false}; + + /** + * Load GuC firmware + */ + bool loadGuCFirmware {false}; + + /** + * Currently loading GuC firmware + */ + bool performingFirmwareLoad {false}; + + /** + * Framebuffer address space start + */ + uint8_t *framebufferStart {nullptr}; + + /** + * Framebuffer address space size + */ + size_t framebufferSize {0}; + + /** + * Pointer to the original GuC firmware + */ + uint8_t *gKmGen9GuCBinary {nullptr}; + + /** + * Pointer to the original GuC firmware signature + */ + uint8_t *signaturePointer {nullptr}; + + /** + * Pointer to GuC firmware upload size assignment + */ + uint32_t *firmwareSizePointer {nullptr}; + + /** + * Dummy firmware buffer to store unused old firmware in + */ + uint8_t *dummyFirmwareBuffer {nullptr}; + + /** + * Actual firmware buffer we store our new firmware in + */ + uint8_t *realFirmwareBuffer {nullptr}; + + /** + * Actual intercepted binary sizes + */ + uint32_t realBinarySize {}; + + /** + * Store backlight level + */ + uint32_t backlightLevel {}; + + /** + * Fallback user-requested backlight frequency in case 0 was initially written to the register. + */ + static constexpr uint32_t FallbackTargetBacklightFrequency {120000}; + + /** + * User-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 at system start. + * Can be specified via max-backlight-freq property. + */ + uint32_t targetBacklightFrequency {}; + + /** + * User-requested pwm control value obtained from BXT_BLC_PWM_CTL1. + */ + uint32_t targetPwmControl {}; + + /** + * Driver-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 write attempt at system start. + */ + uint32_t driverBacklightFrequency {}; + + /** + * Represents the first 16 fields of the receiver capabilities defined in DPCD + * + * Main Reference: + * - DisplayPort Specification Version 1.2 + * + * Side Reference: + * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) + * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h + */ + struct DPCDCap16 // 16 bytes + { + // DPCD Revision (DP Config Version) + // Value: 0x10, 0x11, 0x12, 0x13, 0x14 + uint8_t revision; + + // Maximum Link Rate + // Value: 0x1E (HBR3) 8.1 Gbps + // 0x14 (HBR2) 5.4 Gbps + // 0x0C (3_24) 3.24 Gbps + // 0x0A (HBR) 2.7 Gbps + // 0x06 (RBR) 1.62 Gbps + // Reference: 0x0C is used by Apple internally. + uint8_t maxLinkRate; + + // Maximum Number of Lanes + // Value: 0x1 (HBR2) + // 0x2 (HBR) + // 0x4 (RBR) + // Side Notes: + // (1) Bit 7 is used to indicate whether the link is capable of enhanced framing. + // (2) Bit 6 is used to indicate whether TPS3 is supported. + uint8_t maxLaneCount; + + // Maximum Downspread + uint8_t maxDownspread; + + // Other fields omitted in this struct + // Detailed information can be found in the specification + uint8_t others[12]; + }; + + /** + * User-specified maximum link rate value in the DPCD buffer + * + * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display + */ uint32_t maxLinkRate {0x14}; - - /** - * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer - */ - static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); - - /** - * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates - */ - static IOReturn wrapPavpSessionCallback(void *intelAccelerator, int32_t sessionCommand, uint32_t sessionAppId, uint32_t *a4, bool flag); - - /** - * Global page table read wrapper for Kaby Lake. - */ - static bool globalPageTableRead(void *hardwareGlobalPageTable, uint64_t a1, uint64_t &a2, uint64_t &a3); - - /** - * DP ComputeLaneCount wrapper to report success on non-DP screens to avoid black screen - */ - static bool wrapComputeLaneCount(void *that, void *timing, uint32_t bpp, int32_t availableLanes, int32_t *laneCount); - - /** - * DP ComputeLaneCount wrapper to report success on non-DP screens to avoid black screen (10.14.1+ KBL/CFL version) - */ - static bool wrapComputeLaneCountNouveau(void *that, void *timing, int32_t availableLanes, int32_t *laneCount); - - /** - * copyExistingServices wrapper used to rename Gen6Accelerator from userspace calls - */ - static OSObject *wrapCopyExistingServices(OSDictionary *matching, IOOptionBits inState, IOOptionBits options); - - /** - * IntelAccelerator::start wrapper to support vesa mode, force OpenGL, prevent fw loading, etc. - */ - static bool wrapAcceleratorStart(IOService *that, IOService *provider); - - /** - * Wrapped AppleIntelFramebufferController::WriteRegister32 function - */ - static void wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value); - static void wrapKblWriteRegister32(void *that, uint32_t reg, uint32_t value); - - /** - * AppleIntelFramebufferController::getOSInformation wrapper to patch framebuffer data - */ - static bool wrapGetOSInformation(void *that); - - /** - * IGHardwareGuC::loadGuCBinary wrapper to feed updated (compatible GuC) - */ - static bool wrapLoadGuCBinary(void *that, bool flag); - - /** - * Actual firmware loader - * - * @param that IGScheduler4 instance - * - * @return true on success - */ - static bool wrapLoadFirmware(IOService *that); - - /** - * Handle sleep event - * - * @param that IGScheduler4 instance - */ - static void wrapSystemWillSleep(IOService *that); - - /** - * Handle wake event - * - * @param that IGScheduler4 instance - */ - static void wrapSystemDidWake(IOService *that); - - /** - * IGHardwareGuC::initSchedControl wrapper to avoid incompatibilities during GuC load - */ - static bool wrapInitSchedControl(void *that, void *ctrl); - - /** - * IGSharedMappedBuffer::withOptions wrapper to prepare for GuC firmware loading - */ - static void *wrapIgBufferWithOptions(void *accelTask, unsigned long size, unsigned int type, unsigned int flags); - - /** - * IGMappedBuffer::getGPUVirtualAddress wrapper to trick GuC firmware virtual addresses - */ - static uint64_t wrapIgBufferGetGpuVirtualAddress(void *that); - - /** - * Load GuC-specific patches and hooks - * - * @param patcher KernelPatcher instance - * @param index kinfo handle for graphics driver - * @param address kinfo load address - * @param size kinfo memory size - */ - void loadIGScheduler4Patches(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); - - /** - * Load user-specified arguments from IGPU device - * - * @param igpu IGPU device handle - * @param currentFramebuffer current framebuffer id number - * - * @return true if there is anything to do - */ - bool loadPatchesFromDevice(IORegistryEntry *igpu, uint32_t currentFramebuffer); - - /** - * Find the framebuffer id in data - * - * @param framebufferId Framebuffer id to search - * @param startingAddress Start address of data to search - * @param maxSize Maximum size of data to search - * - * @return pointer to address in data or nullptr - */ - uint8_t *findFramebufferId(uint32_t framebufferId, uint8_t *startingAddress, size_t maxSize); + + /** + * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer + */ + static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); + + /** + * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates + */ + static IOReturn wrapPavpSessionCallback(void *intelAccelerator, int32_t sessionCommand, uint32_t sessionAppId, uint32_t *a4, bool flag); + + /** + * Global page table read wrapper for Kaby Lake. + */ + static bool globalPageTableRead(void *hardwareGlobalPageTable, uint64_t a1, uint64_t &a2, uint64_t &a3); + + /** + * DP ComputeLaneCount wrapper to report success on non-DP screens to avoid black screen + */ + static bool wrapComputeLaneCount(void *that, void *timing, uint32_t bpp, int32_t availableLanes, int32_t *laneCount); + + /** + * DP ComputeLaneCount wrapper to report success on non-DP screens to avoid black screen (10.14.1+ KBL/CFL version) + */ + static bool wrapComputeLaneCountNouveau(void *that, void *timing, int32_t availableLanes, int32_t *laneCount); + + /** + * copyExistingServices wrapper used to rename Gen6Accelerator from userspace calls + */ + static OSObject *wrapCopyExistingServices(OSDictionary *matching, IOOptionBits inState, IOOptionBits options); + + /** + * IntelAccelerator::start wrapper to support vesa mode, force OpenGL, prevent fw loading, etc. + */ + static bool wrapAcceleratorStart(IOService *that, IOService *provider); + + /** + * Wrapped AppleIntelFramebufferController::WriteRegister32 function + */ + static void wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value); + static void wrapKblWriteRegister32(void *that, uint32_t reg, uint32_t value); + + /** + * AppleIntelFramebufferController::getOSInformation wrapper to patch framebuffer data + */ + static bool wrapGetOSInformation(void *that); + + /** + * IGHardwareGuC::loadGuCBinary wrapper to feed updated (compatible GuC) + */ + static bool wrapLoadGuCBinary(void *that, bool flag); + + /** + * Actual firmware loader + * + * @param that IGScheduler4 instance + * + * @return true on success + */ + static bool wrapLoadFirmware(IOService *that); + + /** + * Handle sleep event + * + * @param that IGScheduler4 instance + */ + static void wrapSystemWillSleep(IOService *that); + + /** + * Handle wake event + * + * @param that IGScheduler4 instance + */ + static void wrapSystemDidWake(IOService *that); + + /** + * IGHardwareGuC::initSchedControl wrapper to avoid incompatibilities during GuC load + */ + static bool wrapInitSchedControl(void *that, void *ctrl); + + /** + * IGSharedMappedBuffer::withOptions wrapper to prepare for GuC firmware loading + */ + static void *wrapIgBufferWithOptions(void *accelTask, unsigned long size, unsigned int type, unsigned int flags); + + /** + * IGMappedBuffer::getGPUVirtualAddress wrapper to trick GuC firmware virtual addresses + */ + static uint64_t wrapIgBufferGetGpuVirtualAddress(void *that); + + /** + * Load GuC-specific patches and hooks + * + * @param patcher KernelPatcher instance + * @param index kinfo handle for graphics driver + * @param address kinfo load address + * @param size kinfo memory size + */ + void loadIGScheduler4Patches(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); + + /** + * Load user-specified arguments from IGPU device + * + * @param igpu IGPU device handle + * @param currentFramebuffer current framebuffer id number + * + * @return true if there is anything to do + */ + bool loadPatchesFromDevice(IORegistryEntry *igpu, uint32_t currentFramebuffer); + + /** + * Find the framebuffer id in data + * + * @param framebufferId Framebuffer id to search + * @param startingAddress Start address of data to search + * @param maxSize Maximum size of data to search + * + * @return pointer to address in data or nullptr + */ + uint8_t *findFramebufferId(uint32_t framebufferId, uint8_t *startingAddress, size_t maxSize); #ifdef DEBUG - /** - * Calculate total size of platform table list, including termination entry (FFFFFFFF 00000000) - * - * @param maxSize Maximum size of data to search - * - * @return size of data - */ - size_t calculatePlatformListSize(size_t maxSize); - - /** - * Write platform table data to ioreg - * - * @param subKeyName ioreg subkey (under IOService://IOResources/WhateverGreen) - */ - void writePlatformListData(const char *subKeyName); + /** + * Calculate total size of platform table list, including termination entry (FFFFFFFF 00000000) + * + * @param maxSize Maximum size of data to search + * + * @return size of data + */ + size_t calculatePlatformListSize(size_t maxSize); + + /** + * Write platform table data to ioreg + * + * @param subKeyName ioreg subkey (under IOService://IOResources/WhateverGreen) + */ + void writePlatformListData(const char *subKeyName); #endif - /** - * Patch data without changing kernel protection - * - * @param patch KernelPatcher instance - * @param startingAddress Start address of data to search - * @param maxSize Maximum size of data to search - * - * @return true if patched anything - */ - bool applyPatch(const KernelPatcher::LookupPatch &patch, uint8_t *startingAddress, size_t maxSize); - - /** - * Patch platformInformationList - * - * @param framebufferId Framebuffer id - * @param platformInformationList PlatformInformationList pointer - * - * @return true if patched anything - */ - template - bool applyPlatformInformationListPatch(uint32_t framebufferId, T *platformInformationList); - - /** - * Extended patching called from applyPlatformInformationListPatch - * - * @param frame pointer to Framebuffer data - * - */ - template - void applyPlatformInformationPatchEx(T* frame); - - /** - * Apply framebuffer patches - */ - void applyFramebufferPatches(); - - /** - * Patch platformInformationList with DP to HDMI connector type replacements - * - * @param framebufferId Framebuffer id - * @param platformInformationList PlatformInformationList pointer - * - * @return true if patched anything - */ - template - bool applyDPtoHDMIPatch(uint32_t framebufferId, T *platformInformationList); - - /** - * Apply DP to HDMI automatic connector type changes - */ - void applyHdmiAutopatch(); + /** + * Patch data without changing kernel protection + * + * @param patch KernelPatcher instance + * @param startingAddress Start address of data to search + * @param maxSize Maximum size of data to search + * + * @return true if patched anything + */ + bool applyPatch(const KernelPatcher::LookupPatch &patch, uint8_t *startingAddress, size_t maxSize); + + /** + * Patch platformInformationList + * + * @param framebufferId Framebuffer id + * @param platformInformationList PlatformInformationList pointer + * + * @return true if patched anything + */ + template + bool applyPlatformInformationListPatch(uint32_t framebufferId, T *platformInformationList); + + /** + * Extended patching called from applyPlatformInformationListPatch + * + * @param frame pointer to Framebuffer data + * + */ + template + void applyPlatformInformationPatchEx(T* frame); + + /** + * Apply framebuffer patches + */ + void applyFramebufferPatches(); + + /** + * Patch platformInformationList with DP to HDMI connector type replacements + * + * @param framebufferId Framebuffer id + * @param platformInformationList PlatformInformationList pointer + * + * @return true if patched anything + */ + template + bool applyDPtoHDMIPatch(uint32_t framebufferId, T *platformInformationList); + + /** + * Apply DP to HDMI automatic connector type changes + */ + void applyHdmiAutopatch(); }; #endif /* kern_igfx_hpp */ From 8f954caf5e5676cedc0d4333a5535bb306ce952a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 14:08:08 +0800 Subject: [PATCH 009/102] Add the driver support for onboard LSPCON chips to enable DisplayPort to HDMI 2.0 output. --- WhateverGreen/kern_igfx.cpp | 440 ++++++++++++++++++++++++++- WhateverGreen/kern_igfx.hpp | 576 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1014 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 07d7d70c..5e4f4a8e 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -141,7 +141,12 @@ void IGFX::init() { } void IGFX::deinit() { - + for (int index = 0; index < arrsize(lspcons); index++) { + if (lspcons[index].lspcon != nullptr) { + delete lspcons[index].lspcon; + lspcons[index].lspcon = nullptr; + } + } } void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { @@ -160,6 +165,30 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { dumpPlatformTable = true; #endif + // Enable the LSPCON driver support if the corresponding boot argument is found + supportLSPCON = checkKernelArgument("-igfxlspcon"); + // Or if "enable-lspcon-support" is set in IGPU property + if (!supportLSPCON) + supportLSPCON = info->videoBuiltin->getProperty("enable-lspcon-support") != nullptr; + + // Read the user-defined IGPU properties to know whether a connector has an onboard LSPCON chip + if (supportLSPCON) { + char name[48]; + uint32_t pmode = 0x01; // PCON mode as a fallback value + for (size_t index = 0; index < arrsize(lspcons); index++) { + bzero(name, 48); + snprintf(name, sizeof(name), "framebuffer-con%lu-has-lspcon", index); + WIOKit::getOSDataValue(info->videoBuiltin, name, lspcons[index].hasLSPCON); + snprintf(name, sizeof(name), "framebuffer-con%lu-preferred-lspcon-mode", index); + WIOKit::getOSDataValue(info->videoBuiltin, name, pmode); + // Assuming PCON mode if invalid mode value (i.e. > 1) specified by the user + lspcons[index].preferredMode = LSPCON::parseMode(pmode != 0); + } + } + + // Enable the verbose output in I2C-over-AUX transactions if the corresponding boot argument is found + verboseI2C = checkKernelArgument("-igfxi2cdbg"); + // Enable maximum link rate patch if the corresponding boot argument is found maxLinkRatePatch = checkKernelArgument("-igfxmlr"); // Or if "enable-dpcd-max-link-rate-fix" is set in IGPU property @@ -227,7 +256,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { hdmiAutopatch = !applyFramebufferPatch && !connectorLessFrame && getKernelVersion() >= Yosemite && !checkKernelArgument("-igfxnohdmi"); // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch; + switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch && !supportLSPCON; switchOffGraphics = !pavpDisablePatch && !forceOpenGL && !moderniseAccelerator && !avoidFirmwareLoading && !readDescriptorPatch; } else { switchOffGraphics = switchOffFramebuffer = true; @@ -376,6 +405,28 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } } + if (supportLSPCON) { + auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); + auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); + auto gdi = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", address, size); + if (roa && woa && gdi) { + patcher.eraseCoverageInstPrefix(roa); + patcher.eraseCoverageInstPrefix(woa); + patcher.eraseCoverageInstPrefix(gdi); + orgReadI2COverAUX = reinterpret_cast(patcher.routeFunction(roa, reinterpret_cast(wrapReadI2COverAUX), true)); + orgWriteI2COverAUX = reinterpret_cast(patcher.routeFunction(woa, reinterpret_cast(wrapWriteI2COverAUX), true)); + orgGetDPCDInfo = reinterpret_cast(patcher.routeFunction(gdi, reinterpret_cast(wrapGetDPCDInfo), true)); + if (orgReadI2COverAUX && orgWriteI2COverAUX && orgGetDPCDInfo) { + DBGLOG("igfx", "SC: ReadI2COverAUX(), etc. have been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "SC: ReadI2COverAUX(), etc. cannot be routed."); + } + } else { + SYSLOG("igfx", "SC: Failed to find ReadI2COverAUX(), etc."); + } + } + if (blackScreenPatch) { bool foundSymbol = false; @@ -651,6 +702,391 @@ int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address return retVal; } +IGFX::LSPCON::LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath) { + this->controller = controller; + this->framebuffer = framebuffer; + this->displayPath = displayPath; + this->isActive = false; + this->index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); +} + +IOReturn IGFX::LSPCON::probe() { + // Read the adapter info + uint8_t buffer[128]; + bzero(buffer, 128); + IOReturn retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, 0x00, 128, buffer, 0); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Failed to read the LSPCON adapter info. RV = 0x%llx.", index, retVal); + return retVal; + } + + // Start to parse the adapter info + auto info = reinterpret_cast(buffer); + // Guard: Check whether this is a LSPCON adapter + if (!isLSPCONAdapter(info)) { + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Not a LSPCON DP-HDMI adapter. AdapterID = 0x%02x.", index, info->adapterID); + return kIOReturnNotFound; + } + + // Parse the chip vendor + Vendor vendor = parseVendor(info); + char device[8]; + bzero(device, 8); + lilu_os_memcpy(device, info->deviceID, 6); + DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, getVendorString(vendor), device); + + // Parse the current adapter mode + Mode mode = parseMode(info->lspconCurrentMode); + DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] The current adapter mode is %s.", index, getModeString(mode)); + switch (mode) { + case Mode::LevelShifter: + break; + + case Mode::ProtocolConverter: + isActive = true; + break; + + default: + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); + break; + } + + return kIOReturnSuccess; +} + +IOReturn IGFX::LSPCON::getMode(Mode *mode) { + uint8_t byte; + IOReturn retVal = kIOReturnAborted; + + // Guard: The given `mode` pointer cannot be NULL + if (mode != nullptr) { + // Try at most 5 times + for (int attempt = 0; attempt < 5; attempt++) { + // Read from the adapter @ 0x40; offset = 0x41 + retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CURRENT_MODE, 1, &byte, 0); + + // Guard: Can read the current adapter mode successfully + if (retVal == kIOReturnSuccess) { + DBGLOG("igfx", "SC: LSPCON::getMode() DInfo: [FB%d] The current mode value is 0x%02x.", index, byte); + *mode = parseMode(byte); + break; + } + + // Sleep 1 ms + IOSleep(1); + } + } + + return retVal; +} + +IOReturn IGFX::LSPCON::setMode(Mode newMode) { + // Guard: The given new mode must be valid + if (newMode == Mode::Invalid) + return kIOReturnAborted; + + // Guard: Write the new mode + uint8_t byte = getModeValue(newMode); + IOReturn retVal = callbackIGFX->advWriteI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CHANGE_MODE, 1, &byte, 0); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to set the new adapter mode. RV = 0x%llx.", index, retVal); + return retVal; + } + + // Read the register again and verify the mode + uint32_t timeout = 200; + Mode mode = Mode::Invalid; + while (timeout != 0) { + retVal = getMode(&mode); + // Guard: Read the current effective mode + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to read the new effective mode. RV = 0x%llx.", index, retVal); + continue; + } + // Guard: The new mode is effective now + if (mode == newMode) { + DBGLOG("igfx", "SC: LSPCON::setMode() DInfo: [FB%d] The new mode is now effective.", index); + isActive = newMode == Mode::ProtocolConverter; + return kIOReturnSuccess; + } + timeout -= 20; + IOSleep(20); + } + + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Timed out while waiting for the new mode to be effective. Last RV = 0x%llx.", index, retVal); + return retVal; +} + +IOReturn IGFX::LSPCON::setModeIfNecessary(Mode newMode) { + if (isRunningInMode(newMode)) { + DBGLOG("igfx", "SC: LSPCON::setModeIfNecessary() DInfo: [FB%d] The adapter is already running in %s mode. No need to update.", index, getModeString(newMode)); + return kIOReturnSuccess; + } + + return setMode(newMode); +} + +IOReturn IGFX::LSPCON::wakeUpNativeAUX() { + uint8_t byte; + IOReturn retVal = wrapReadAUX(controller, framebuffer, 0x00000, 1, &byte, displayPath); + if (retVal != kIOReturnSuccess) + SYSLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() Error: [FB%d] Failed to wake up the native AUX channel. RV = 0x%llx.", index, retVal); + else + DBGLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() DInfo: [FB%d] The native AUX channel is up. DPCD Rev = 0x%02x.", index, byte); + return retVal; +} + +IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { + if (callbackIGFX->verboseI2C) { + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + DBGLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", + index->unsigned32BitValue(), address, length, intermediate, flags); + IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); + DBGLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); + return retVal; + } else { + return callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); + } +} + +IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { + if (callbackIGFX->verboseI2C) { + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + DBGLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", + index->unsigned32BitValue(), address, length, intermediate); + IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); + DBGLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); + return retVal; + } else { + return callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); + } +} + +IOReturn IGFX::advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags) { + // No need to check the given `address` and `offset` + // if they are invalid, the underlying RunAUXCommand() will return an error + // First start the transaction by performing an empty write + IOReturn retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, true); + + // Guard: Check the START transaction + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: AdvSeekI2COverAUX() Error: Failed to start the I2C transaction. Return value = 0x%x.\n", retVal); + return retVal; + } + + // Write a single byte to the given I2C slave + // and set the Middle-of-Transaction bit to 1 + return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 1, reinterpret_cast(&offset), true); +} + +IOReturn IGFX::advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { + // Guard: Check the buffer length + if (length == 0) { + SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Buffer length must be non-zero."); + return kIOReturnInvalid; + } + + // Guard: Check the buffer + if (buffer == nullptr) { + SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Buffer cannot be NULL."); + return kIOReturnInvalid; + } + + // Guard: Start the transaction and set the access offset successfully + IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Failed to set the data offset."); + return retVal; + } + + // Process the read request + // ReadI2COverAUX() can only process up to 16 bytes in one AUX transaction + // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header + while (length != 0) { + // Calculate the new length for this I2C-over-AUX transaction + uint16_t newLength = length >= 16 ? 16 : length; + + // This is an intermediate transaction + retVal = wrapReadI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true, flags); + + // Guard: The intermediate transaction succeeded + if (retVal != kIOReturnSuccess) { + // Terminate the transaction + wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); + return retVal; + } + + // Update the buffer position and length + length -= newLength; + buffer += newLength; + } + + // All intermediate transactions succeeded + // Terminate the transaction + return wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); +} + +IOReturn IGFX::advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { + // Guard: Check the buffer length + if (length == 0) { + SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Buffer length must be non-zero."); + return kIOReturnInvalid; + } + + // Guard: Check the buffer + if (buffer == nullptr) { + SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Buffer cannot be NULL."); + return kIOReturnInvalid; + } + + // Guard: Start the transaction and set the access offset successfully + IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Failed to set the data offset."); + return retVal; + } + + // Process the write request + // WriteI2COverAUX() can only process up to 16 bytes in one AUX transaction + // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header + while (length != 0) { + // Calculate the new length for this I2C-over-AUX transaction + uint16_t newLength = length >= 16 ? 16 : length; + + // This is an intermediate transaction + retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true); + + // Guard: The intermediate transaction succeeded + if (retVal != kIOReturnSuccess) { + // Terminate the transaction + wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); + return retVal; + } + + // Update the buffer position and length + length -= newLength; + buffer += newLength; + } + + // All intermediate transactions succeeded + // Terminate the transaction + return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); +} + +IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath) { + // + // Abstract + // + // Recent laptops are now typically equipped with a HDMI 2.0 port. + // For laptops with Intel IGPU + Nvidia DGPU, this HDMI 2.0 port could be either routed to IGPU or DGPU, + // and we are only interested in the former case, because DGPU (Optimus) is not supported on macOS. + // However, as indicated in Intel Product Specifications, Intel (U)HD Graphics card does not provide + // native HDMI 2.0 output. As a result, Intel suggests that OEMs should add an additional hardware component + // named LSPCON (Level Shifter and Protocol Converter) on the motherboard to convert DisplayPort to HDMI. + // + // LSPCON conforms to the DisplayPort Dual Mode (DP++) standard and works in either Level Shifter (LS) mode + // or Protocol Converter (PCON) mode. + // When the adapter is running in LS mode, it supports DisplayPort to HDMI 1.4. + // When the adapter is running in PCON mode, it supports DisplayPort to HDMI 2.0. + // LSPCON on some laptops, Dell XPS 15 9570 for example, might be configured in the firmware to run in LS mode + // by default, resulting in a black screen on a HDMI 2.0 connection. + // In comparison, LSPCON on a DisplayPort to HDMI 2.0 cable could be configured to always run in PCON mode. + // Fortunately, LSPCON is programmable and is a Type 2 DP++ adapter, so the graphics driver could communicate + // with the adapter via either the native I2C protocol or the special I2C-over-AUX protocol to configure the + // mode properly for HDMI connections. + // + // This reverse engineering research analyzes and exploits Apple's existing I2C-over-AUX transaction API layers, + // and an additional API layer is implemented to provide R/W access to registers at specific offsets on the adapter. + // AppleIntelFramebufferController::GetDPCDInfo() is then wrapped to inject code for LSPCON driver initialization, + // so that the adapter is configured to run in PCON mode instead on plugging the HDMI 2.0 cable to the port. + // Besides, the adapter is able to handle HDMI 1.4 connections when running in PCON mode, so we don't need to switch + // back to LS mode again. + // + // If you are interested in the detailed theory behind this fix, please take a look at my blog post. + // [ToDo: The article is still working in progress. I will add the link at here once I finish it. :D] + // + // Notes: + // 1. This fix is applicable for all laptops and PCs with HDMI 2.0 routed to IGPU. + // (Laptops/Mobiles start from Skylake platform. e.g. Intel Skull Canyon NUC; Iris Pro 580 and HDMI 2.0) + // 2. If your HDMI 2.0 with Intel (U)HD Graphics is working properly, you don't need this fix, + // as the adapter might already be configured in the firmware to run in PCON mode. + // + // - FireWolf + // - 2019.06 + // + + // Retrieve the framebuffer index + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); + + // Retrieve the user preference + LSPCON *lspcon = nullptr; + auto pmode = framebufferLSPCONGetPreferredMode(index); + + // Guard: Check whether this framebuffer connector has an onboard LSPCON chip + if (!framebufferHasLSPCON(index)) { + // No LSPCON chip associated with this connector + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] No LSPCON chip associated with this framebuffer.", index); + goto org; + } + + // Guard: Check whether the LSPCON driver has already been initialized for this framebuffer + if (framebufferHasLSPCONInitialized(index)) { + // Already initialized + lspcon = framebufferGetLSPCON(index); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver (at 0x%llx) has already been initialized for this framebuffer.", index, lspcon); + // Confirm that the adapter is running in preferred mode + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] The adapter is not running in preferred mode. Failed to update the mode.", index); + } + // Wake up the native AUX channel if PCON mode is preferred + if (pmode == LSPCON::Mode::ProtocolConverter) { + if (lspcon->wakeUpNativeAUX() != kIOReturnSuccess) + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to wake up the native AUX channel.", index); + } + goto org; + } + + // User has specified the existence of onboard LSPCON + // Guard: Initialize the driver for this framebuffer + lspcon = new LSPCON(that, framebuffer, displayPath); + if (lspcon == nullptr) { + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to initialize the LSPCON driver. Insufficient memory.", index); + goto org; + } + + // Guard: Attempt to probe the onboard LSPCON chip + if (lspcon->probe() != kIOReturnSuccess) { + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to probe the LSPCON adapter.", index); + delete lspcon; + lspcon = nullptr; + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Abort the LSPCON driver initialization.", index); + goto org; + } + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver has detected the onboard chip successfully.", index); + + // LSPCON driver has been initialized successfully + framebufferSetLSPCON(index, lspcon); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver has been initialized successfully.", index); + + // Guard: Set the preferred adapter mode if necessary + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] The adapter is not running in preferred mode. Failed to set the %s mode.", index, LSPCON::getModeString(pmode)); + } + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] The adapter is running in preferred mode [%s].", index, LSPCON::getModeString(pmode)); + +#ifdef DEBUG + framebuffer->setProperty("fw-framebuffer-has-lspcon", framebufferHasLSPCON(index)); + framebuffer->setProperty("fw-framebuffer-preferred-lspcon-mode", LSPCON::getModeValue(pmode), 8); +#endif + +org: + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] Will call the original method.", index); + IOReturn retVal = callbackIGFX->orgGetDPCDInfo(that, framebuffer, displayPath); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] Returns 0x%llx.", index, retVal); + return retVal; +} + void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { if (reg == BXT_BLC_PWM_FREQ1) { if (value && value != callbackIGFX->driverBacklightFrequency) { diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 524bf13f..a57095a5 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -251,6 +251,21 @@ class IGFX { * Original AppleIntelFramebufferController::ReadAUX function */ int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; + + /** + * Original AppleIntelFramebufferController::ReadI2COverAUX function + */ + IOReturn (*orgReadI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool, uint8_t) {nullptr}; + + /** + * Original AppleIntelFramebufferController::WriteI2COverAUX function + */ + IOReturn (*orgWriteI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool) {nullptr}; + + /** + * Original AppleIntelFramebufferController::GetDPCDInfo function + */ + IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *); /** * Detected CPU generation of the host system @@ -284,6 +299,16 @@ class IGFX { */ bool maxLinkRatePatch {false}; + /** + * Set to true to enable LSPCON driver support + */ + bool supportLSPCON {false}; + + /** + * Set to true to enable verbose output in I2C-over-AUX transactions + */ + bool verboseI2C {false}; + /** * Set to true if PAVP code should be disabled */ @@ -458,6 +483,557 @@ class IGFX { * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer */ static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); + + /** + * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 + * + * Specific to LSPCON DisplayPort 1.2 to HDMI 2.0 Adapter + */ + struct DisplayPortDualModeAdapterInfo // first 128 bytes + { + /// [0x00] HDMI ID + /// + /// Fixed Value: "DP-HDMI ADAPTOR\x04" + uint8_t hdmiID[16]; + + /// [0x10] Adapter ID + /// + /// Bit Masks: + /// - isType2 = 0xA0 + /// - hasDPCD = 0x08 + /// + /// Sample Values: 0xA8 = Type 2 Adapter with DPCD + uint8_t adapterID; + + /// [0x11] IEEE OUI + /// + /// Sample Value: 0x001CF8 [Parade] + /// Reference: http://standards-oui.ieee.org/oui.txt + uint8_t oui[3]; + + /// [0x14] Device ID + /// + /// Sample Value: 0x505331373530 = "PS1750" + uint8_t deviceID[6]; + + /// [0x1A] Hardware Revision Number + /// + /// Sample Value: 0xB2 (B2 version) + uint8_t revision; + + /// [0x1B] Firmware Major Revision + uint8_t firmwareMajor; + + /// [0x1C] Firmware Minor Revision + uint8_t firmwareMinor; + + /// [0x1D] Maximum TMDS Clock + uint8_t maxTMDSClock; + + /// [0x1E] I2C Speed Capability + uint8_t i2cSpeedCap; + + /// [0x1F] Unused/Reserved Field??? + uint8_t reserved0; + + /// [0x20] TMDS Output Buffer State + /// + /// Bit Masks: + /// - Disabled = 0x01 + /// + /// Sample Value: + /// 0x00 = Enabled + uint8_t tmdsOutputBufferState; + + /// [0x21] HDMI PIN CONTROL + uint8_t hdmiPinCtrl; + + /// [0x22] I2C Speed Control + uint8_t i2cSpeedCtrl; + + /// [0x23 - 0x3F] Unused/Reserved Fields + uint8_t reserved1[29]; + + /// [0x40] [W] Set the new LSPCON mode + uint8_t lspconChangeMode; + + /// [0x41] [R] Get the current LSPCON mode + /// + /// Bit Masks: + /// - PCON = 0x01 + /// + /// Sample Value: + /// 0x00 = LS + /// 0x01 = PCON + uint8_t lspconCurrentMode; + + /// [0x42 - 0x7F] Rest Unused/Reserved Fields + uint8_t reserved2[62]; + }; + + /** + * Represents the onboard Level Shifter and Protocol Converter + */ + class LSPCON + { + public: + /** + * Represents all possible adapter modes + */ + enum class Mode: uint32_t { + + /// Level Shifter Mode (DP++ to HDMI 1.4) + LevelShifter = 0x00, + + /// Protocol Converter Mode (DP++ to HDMI 2.0) + ProtocolConverter = 0x01, + + /// Invalid Mode + Invalid + }; + + /** + * [Mode Helper] Parse the adapter mode from the raw register value + * + * @param mode A raw register value read from the adapter. + * @return The corresponding adapter mode on success, `invalid` otherwise. + */ + static inline Mode parseMode(uint8_t mode) { + switch (mode & DP_DUAL_MODE_LSPCON_MODE_PCON) { + case DP_DUAL_MODE_LSPCON_MODE_LS: + return Mode::LevelShifter; + + case DP_DUAL_MODE_LSPCON_MODE_PCON: + return Mode::ProtocolConverter; + + default: + return Mode::Invalid; + } + } + + /** + * [Mode Helper] Get the raw value of the given adapter mode + * + * @param mode A valid adapter mode + * @return The corresponding register value on success. + * If the given mode is `Invalid`, the raw value of `LS` mode will be returned. + */ + static inline uint8_t getModeValue(Mode mode) { + switch (mode) { + case Mode::LevelShifter: + return DP_DUAL_MODE_LSPCON_MODE_LS; + + case Mode::ProtocolConverter: + return DP_DUAL_MODE_LSPCON_MODE_PCON; + + default: + return DP_DUAL_MODE_LSPCON_MODE_LS; + } + } + + /** + * [Mode Helper] Get the string representation of the adapter mode + */ + static inline const char *getModeString(Mode mode) { + switch (mode) { + case Mode::LevelShifter: + return "Level Shifter (DP++ to HDMI 1.4)"; + + case Mode::ProtocolConverter: + return "Protocol Converter (DP++ to HDMI 2.0)"; + + default: + return "Invalid"; + } + } + + /** + * Initialize the LSPCON chip for the given framebuffer + * + * @param controller The opaque `AppleIntelFramebufferController` instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + */ + LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath); + + /** + * Probe the onboard LSPCON chip + * + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method will mark the adapter active if it has found that the adpater is already in `PCON` mode. + * @see `isActive` and `isAdapterActive()`. + */ + IOReturn probe(); + + /** + * Get the current adapter mode + * + * @param mode The current adapter mode on return. + * @return `kIOReturnSuccess` on success, errors otherwise. + */ + IOReturn getMode(Mode *mode); + + /** + * Change the adapter mode + * + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method will not return until `newMode` is effective. + * @note This method will mark the adapter active if `newMode` is `PCON`and becomes effective. + * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. + */ + IOReturn setMode(Mode newMode); + + /** + * Change the adapter mode if necessary + * + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method is a wrapper of `setMode` and will only set the new mode if `newMode` is not currently effective. + * @seealso `setMode(newMode:)` + */ + IOReturn setModeIfNecessary(Mode newMode); + + /** + * Wake up the native DisplayPort AUX channel for this adapter + * + * @return `kIOReturnSuccess` on success, other errors otherwise. + */ + IOReturn wakeUpNativeAUX(); + + /** + * Return `true` if the adapter is active + */ + inline bool isAdapterActive() { + return this->isActive; + } + + /** + * Return `true` if the adapter is running in the given mode + * + * @param mode The expected mode; one of `LS` and `PCON` + */ + inline bool isRunningInMode(Mode mode) { + // isActive 1 1 0 0 + // Preferred 1 0 1 0 + // XOR 0 1 1 0 + return !(isActive ^ getModeValue(mode)); + } + + private: + /// The 7-bit I2C slave address of the DisplayPort dual mode adapter + static constexpr uint32_t DP_DUAL_MODE_ADAPTER_I2C_ADDR = 0x40; + + /// Register address to change the adapter mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_CHANGE_MODE = 0x40; + + /// Register address to read the current adapter mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_CURRENT_MODE = 0x41; + + /// Register value when the adapter is in **Level Shifter** mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_LS = 0x00; + + /// Register value when the adapter is in **Protocol Converter** mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_PCON = 0x01; + + /// IEEE OUI of Parade Technologies + static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_PARADE = 0x001CF8; + + /// IEEE OUI of MegaChips Corporation + static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS = 0x0060AD; + + /// Bit mask indicating that the DisplayPort dual mode adapter is of type 2 + static constexpr uint8_t DP_DUAL_MODE_TYPE_IS_TYPE2 = 0xA0; + + /// Bit mask indicating that the DisplayPort dual mode adapter has DPCD (LSPCON case) + static constexpr uint8_t DP_DUAL_MODE_TYPE_HAS_DPCD = 0x08; + + /** + * Represents all possible chip vendors + */ + enum class Vendor { + MegaChips, + Parade, + Unknown + }; + + /// The opaque framebuffer controller instance + void *controller; + + /// The framebuffer that owns this LSPCON chip + IORegistryEntry *framebuffer; + + /// The corresponding opaque display path instance + void *displayPath; + + /// Indicate whether the LSPCON adapter is active or not; + /// The adapter is considered to be active once it is switched to PCON mode + bool isActive; + + /// The framebuffer index (for debugging purposes) + uint32_t index; + + /** + * [Vendor Helper] Parse the adapter vendor from the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return The vendor on success, `Unknown` otherwise. + */ + static inline Vendor parseVendor(DisplayPortDualModeAdapterInfo *info) { + uint32_t oui = info->oui[0] << 16 | info->oui[1] << 8 | info->oui[2]; + switch (oui) { + case DP_DUAL_MODE_LSPCON_VENDOR_PARADE: + return Vendor::Parade; + + case DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS: + return Vendor::MegaChips; + + default: + return Vendor::Unknown; + } + } + + /** + * [Vendor Helper] Get the string representation of the adapter vendor + */ + static inline const char *getVendorString(Vendor vendor) { + switch (vendor) { + case Vendor::Parade: + return "Parade"; + + case Vendor::MegaChips: + return "MegaChips"; + + default: + return "Unknown"; + } + } + + /** + * [DP++ Helper] Check whether this is a HDMI adapter based on the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a HDMI adapter, `false` otherwise. + */ + static inline bool isHDMIAdapter(DisplayPortDualModeAdapterInfo *info) { + return memcmp(info->hdmiID, "DP-HDMI ADAPTOR\x04", 16) == 0; + } + + /** + * [DP++ Helper] Check whether this is a LSPCON adapter based on the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a LSPCON DP-HDMI adapter, `false` otherwise. + */ + static inline bool isLSPCONAdapter(DisplayPortDualModeAdapterInfo *info) { + // Guard: Check whether it is a DP to HDMI adapter + if (!isHDMIAdapter(info)) + return false; + + // Onboard LSPCON adapter must be of type 2 and have DPCD info + return info->adapterID == (DP_DUAL_MODE_TYPE_IS_TYPE2 | DP_DUAL_MODE_TYPE_HAS_DPCD); + } + }; + + /** + * Represents the LSPCON chip info for a framebuffer + */ + struct FramebufferLSPCON + { + /** + * Indicate whether this framebuffer has an onboard LSPCON chip + * + * @note This value will be read from the IGPU property `framebuffer-conX-has-lspcon`. + * @warning If not specified, assuming no onboard LSPCON chip for this framebuffer. + */ + uint32_t hasLSPCON {0}; + + /** + * User preferred LSPCON adapter mode + * + * @note This value will be read from the IGPU property `framebuffer-conX-preferred-lspcon-mode`. + * @warning If not specified, assuming `PCON` mode is preferred. + * @warning If invalid mode value found, assuming `PCON` mode + */ + LSPCON::Mode preferredMode {LSPCON::Mode::ProtocolConverter}; + + /** + * The corresponding LSPCON driver; `NULL` if no onboard chip + */ + LSPCON *lspcon {nullptr}; + }; + + /** + * User-defined LSPCON chip info for all possible framebuffers + */ + FramebufferLSPCON lspcons[MaxFramebufferConnectorCount]; + + /// MARK:- Manage user-defined LSPCON chip info for all framebuffers + + /** + * [Convenient] Check whether the given framebuffer has an onboard LSPCON chip + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. + */ + static inline bool framebufferHasLSPCON(uint32_t index) { + return callbackIGFX->lspcons[index].hasLSPCON; + } + + /** + * [Convenient] Check whether the given framebuffer already has LSPCON driver initialized + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. + */ + static inline bool framebufferHasLSPCONInitialized(uint32_t index) { + return callbackIGFX->lspcons[index].lspcon != nullptr; + } + + /** + * [Convenient] Get the non-null LSPCON driver associated with the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The LSPCON driver instance. + */ + static inline LSPCON *framebufferGetLSPCON(uint32_t index) { + return callbackIGFX->lspcons[index].lspcon; + } + + /** + * [Convenient] Set the non-null LSPCON driver for the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer + */ + static inline void framebufferSetLSPCON(uint32_t index, LSPCON *lspcon) { + callbackIGFX->lspcons[index].lspcon = lspcon; + } + + /** + * [Convenient] Get the preferred LSPCON mode for the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The preferred adapter mode. + */ + static inline LSPCON::Mode framebufferLSPCONGetPreferredMode(uint32_t index) { + return callbackIGFX->lspcons[index].preferredMode; + } + + /// MARK:- I2C-over-AUX Transaction APIs + + /** + * [Advanced] Reposition the offset for an I2C-over-AUX access + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset The address of the next register to access + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advSeekI2COverAUX(framebuffer:displayPath:address:offset:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags); + + /** + * [Advanced] Read from an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to read from + * @param length The number of bytes requested to read starting from `offset` + * @param buffer A non-null buffer to store the bytes + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advReadI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); + + /** + * [Advanced] Write to an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to write to + * @param length The number of bytes requested to write starting from `offset` + * @param buffer A non-null buffer containing the bytes to write + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advWriteI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); + + /** + * [Basic] Read from an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to read and must be <= 16 (See below) + * @param buffer A buffer to store the read bytes (See below) + * @param intermediate Set `true` if this is an intermediate read (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::ReadI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:flags:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::ReadI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + * @ref TODO: Add the link to the blog post. [Working In Progress] + */ + static IOReturn wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags); + + /** + * [Basic] Write to an I2C adapter via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to write and must be <= 16 (See below) + * @param buffer A buffer that stores the bytes to write (See below) + * @param intermediate Set `true` if this is an intermediate write (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::WriteI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::WriteI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + * @ref TODO: Add the link to the blog post. [Working In Progress] + */ + static IOReturn wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate); + + /** + * [Wrapper] Retrieve the DPCD info for a given framebuffer port + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::GetDPCDInfo()` method. + * Used to inject code to initialize the driver for the onboard LSPCON chip. + */ + static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates From 8a5cd961c0192dac38ab3d2b7f2cbf254b5933d4 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 14:16:50 +0800 Subject: [PATCH 010/102] Update the change log and fix a typo. --- Changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index dc180a90..6d73649d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ WhateverGreen Changelog - Disabled NVIDIA performance fix on 10.15, as it now is built-in - Enable HDMI 2.0 patches on 10.14+ (Use at own risk in case of undiscovered change) - Added CFL graphics kernel panic workaround on 10.14.4+ +- Added driver support for onboard LSPCON chips to enable DisplayPort to HDMI 2.0 output on Intel IGPUs (by @0xFireWolf) #### v1.2.9 - Added AMD Radeon VII to detected list @@ -17,7 +18,7 @@ WhateverGreen Changelog #### v.1.2.8 - Added KBL graphics kernel panic workaround on 10.14.4+ -- Added IGPU DPDCD link incompatible rate patch (thanks @Firewolf) +- Added IGPU DPCD link incompatible rate patch (thanks @0xFireWolf) #### v1.2.7 - Added more IGPU device-ids to detected list From 42cc393eacaa2debba01e7d499d6998e9347f26f Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 14:24:58 +0800 Subject: [PATCH 011/102] Update the README. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f182b480..1a87ffbd 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ WhateverGreen - Injects IOVARendererID into GPU properties (required for Shiki-based solution for non-freezing Intel and/or any discrete GPU) - For Intel HD digital audio HDMI, DP, Digital DVI (Patches connector-type DP -> HDMI) - Fixes NVIDIA GPU interface stuttering on 10.13 (official and web drivers) +- Fixes the kernel panic caused by an invalid link rate reported by DPCD on some laptops with Intel IGPU. +- Implements the driver support for onboard LSPCON chips to enable DisplayPort to HDMI 2.0 output on some platforms with Intel IGPU. #### Documentation Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) and avoid asking any questions. No support is provided for the time being. @@ -54,12 +56,14 @@ Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) an - `igfxcflbklt=1` boot argument (and `enable-cfl-backlight-fix` property) to enable CFL backlight patch - `applbkl=0` boot argument to disable AppleBacklight.kext patches for IGPU. In case of custom AppleBacklight profile- [read here.](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.OldPlugins.en.md) - `-igfxmlr` boot argument (and `enable-dpcd-max-link-rate-fix` property) to apply the maximum link rate fix. +- `-igfxlspcon` boot argument (and `enable-lspcon-support` property) to enable the driver support for onboard LSPCON chips. [Read the manual](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.IntelHD.en.md) +- `-igfxi2cdbg` boot argument to enable verbose output in I2C-over-AUX transactions (only for debugging purposes). #### Credits - [Apple](https://www.apple.com) for macOS - [AMD](https://www.amd.com) for ATOM VBIOS parsing code - [The PCI ID Repository](http://pci-ids.ucw.cz) for multiple GPU model names -- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix +- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix and LSPCON driver support - [Floris497](https://github.com/Floris497) for the CoreDisplay [patches](https://github.com/Floris497/mac-pixel-clock-patch-v2) - [Fraxul](https://github.com/Fraxul) for original CFL backlight patch - [headkaze](https://github.com/headkaze) for Intel framebuffer patching code and CFL backlight patch improvements From b674d4426116a8110fdd2c2a8a3ca895cd7cac57 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 17:21:36 +0800 Subject: [PATCH 012/102] Added the manual for LSPCON driver support. --- Manual/FAQ.IntelHD.cn.md | 76 ++++++++++++++++++++++++++++++++++++ Manual/FAQ.IntelHD.en.md | 67 +++++++++++++++++++++++++++++++ Manual/Img/lspcon.png | Bin 0 -> 28150 bytes Manual/Img/lspcon_debug.png | Bin 0 -> 93760 bytes 4 files changed, 143 insertions(+) create mode 100644 Manual/Img/lspcon.png create mode 100644 Manual/Img/lspcon_debug.png diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 70b12716..3ca8ac7e 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1734,6 +1734,82 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest 可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 +## 启用 LSPCON 驱动以支持核显 DisplayPort 转 HDMI 2.0 输出 +#### 简述 +近几年的笔记本都开始配备了 HDMI 2.0 输出端口。这个端口可能直接连到核显上也有可能连在独显上。 +如果连在了独显上,那么在 macOS 下这个 HDMI 2.0 端口直接废掉了,因为苹果不支持 Optimus 等双显卡切换技术。 +如果连在了核显上,那么笔记本厂商需要在主板上安装额外的信号转换器来把 DP 信号转换成 HDMI 2.0 信号, +这是因为现阶段英特尔的核显并不能原生提供 HDMI 2.0 信号输出。(类似主板厂商使用第三方芯片以提供 USB 3.0 功能) +这个信号转换器名为 LSPCON,全称 **L**evel **S**hifter and **P**rotocol **Con**verter,并且有两种工作模式。 +当转换器工作在 LS 模式下,它可以把 DP 转换成 HDMI 1.4 信号;在 PCON 模式下,它可以把 DP 转换成 HDMI 2.0 信号。 +然而有些厂商在转换器的固件里把 LS 设为了默认的工作模式,这就导致在 macOS 下 HDMI 2.0 连接直接黑屏或者根本不工作。 +从 1.3.0 版本开始,WhateverGreen 提供了对 LSPCON 的驱动支持。驱动会自动将转换器调为 PCON 模式以解决 HDMI 2.0 输出黑屏问题。 + +#### 使用前必读 +- LSPCON 驱动适用于所有配备 HDMI 2.0 接口并接在核显上的笔记本和台式机。 +- 目前来看,英特尔的新处理器所配备的核显仍然不支持原生 HDMI 2.0 输出,所以在新平台上你可能仍然需要此驱动。 +- 适用的英特尔平台: Skylake, Kaby Lake, Coffee Lake 以及以后。 + Skylake 平台案例: 英特尔在 Skull Canyon NUC 上搭载了 HDMI 2.0 接口,使用了型号为 Parade PS175 的 LSPCON 信号转换器。 + Coffee Lake 平台案例: 部分笔记本如 Dell XPS 15 搭载了 HDMI 2.0 接口,同样使用了型号为 Parade PS175 的 LSPCON 信号转换器。 +- 如果你已确认你的 HDMI 2.0 接口是连在核显上并且目前输出没有任何问题,那么你不需要特意启用此驱动。你的转换器可能已经出厂时就把 PCON 设为了默认的工作模式。 + +#### 如何使用 +- 为核显添加 `enable-lspcon-support` 属性或者直接使用 `-igfxlspcon` 启动参数来启用驱动。 +- 接下来你需要知道 HDMI 2.0 对应的端口号是多少。这个你可以直接在 IORegistryExplorer 里看到。在 AppleIntelFramebuffer@0/1/2/3 下面找到你的外接显示器。 +- 为核显添加 `framebuffer-conX-has-lspcon` 属性来通知驱动哪个接口下面有 LSPCON 信号转换器。把 `conX` 里的 X 替换成你在上一步找到的端口值。 +这个属性的对应值请设为 `Data` 类型。如果接口下存在转换器的话,请设为 `01000000`,反之设为 `00000000`。 +若不定义这个属性的话,驱动默认认为对应接口下**不存在**转换器。 +- (*可选*) 为核显添加 `framebuffer-conX-preferred-lspcon-mode` 属性以指定 LSPCON 信号转换器应该工作在何种模式下。 +这个属性的对应值请设为 `Data` 类型。 +如果希望转换器工作在 PCON (DP 转 HDMI 2.0) 模式下的话,请设为 `01000000`。 +如果希望转换器工作在 LS (DP 转 HDMI 1.4) 模式下的话,请设为 `00000000`。 +若指定其他值的话,驱动默认认为转换器应工作在 PCON 模式下。 +若不定义此属性的话,同上。 +![](Img/lspcon.png) + +#### 排查错误 +完成上述步骤后,重建缓存重启电脑,插上 HDMI 2.0 线和 HDMI 2.0 显示器应该可以正常看到输出的图像了。 +如果提取内核日志的话,你应该可以看到类似如下的日志。 + +``` +// 插入 HDMI 2.0 线 +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a868c000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] No LSPCON chip associated with this framebuffer. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Returns 0x0. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a869a000. +igfx @ (DBG) SC: LSPCON::probe() DInfo: [FB2] Found the LSPCON adapter: Parade PS1750. +igfx @ (DBG) SC: LSPCON::probe() DInfo: [FB2] The current adapter mode is Level Shifter (DP++ to HDMI 1.4). +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver has detected the onboard chip successfully. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver has been initialized successfully. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x01. +igfx @ (DBG) SC: LSPCON::setMode() DInfo: [FB2] The new mode is now effective. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] The adapter is running in preferred mode [Protocol Converter (DP++ to HDMI 2.0)]. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. + +// 拔出 HDMI 2.0 线 +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a868c000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] No LSPCON chip associated with this framebuffer. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Returns 0x0. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a869a000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver (at 0xffffff802ba3afe0) has already been initialized for this framebuffer. +igfx @ (DBG) SC: LSPCON::setModeIfNecessary() DInfo: [FB2] The adapter is already running in Protocol Converter (DP++ to HDMI 2.0) mode. No need to update. +igfx @ (DBG) SC: LSPCON::wakeUpNativeAUX() DInfo: [FB2] The native AUX channel is up. DPCD Rev = 0x12. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. +``` + +另外你也能在 IORegistryExplorer 下找到驱动在对应的 Framebuffer 下注入的属性。(此功能仅限 DEBUG 版驱动) +`fw-framebuffer-has-lspcon` 显示当前端口是否存在 LSPCON 信号转换器,为布尔值类型。 +`fw-framebuffer-preferred-lspcon-mode` 显示当前指定的 LSPCON 工作模式,为数据类型。1 为 PCON 模式,0 为 LS 模式。 +![](Img/lspcon_debug.png) + + ## 已知问题 *兼容性*: - 受限制的显卡:HD2000 和 HD2500,它们只能用于 IQSV (因为在白苹果中它们只用来干这个),无解。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index b5176182..a7574361 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1665,6 +1665,73 @@ All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HB If an invalid value is specified, the default value `0x14` will be used instead. If this property is not specified, same as above. +## LSPCON driver support to enable DisplayPort to HDMI 2.0 output on Intel IGPU +#### Brief Introduction +Recent laptops (Kaby Lake/Coffee Lake-based) are typically equipped with a HDMI 2.0 port. This port could be either routed to IGPU or DGPU, and you can have a confirmation on Windows 10. Intel (U)HD Graphics, however, does not provide native HDMI 2.0 output, so in order to solve this issue OEMs add an additional hardware named LSPCON on the motherboard to convert DisplayPort into HDMI 2.0. +LSPCON works in either Level Shifter (LS) or Protocol Converter (PCON) mode. When the adapter works in LS mode, it is capable of producing HDMI 1.4 signals from DisplayPort, while in PCON mode, it could provide HDMI 2.0 output. Some onboard LSPCON adapters (e.g. the one on Dell XPS 15 9570) have been configured in the firmware to work in LS mode by default, resulting a black screen on handling HDMI 2.0 connections. +Starting from version 1.3.0, WhateverGreen now provides driver support for the onboard LSPCON by automatically configuring the adapter to run in PCON mode on new HDMI connections, and hence solves the black screen issue on some platforms. +#### Before you start +- LSPCON driver is only applicable for laptops and PCs **with HDMI 2.0 routed to Intel IGPU**. +- LSPCON driver is necessary for all newer platforms unless the new Intel IGPU starts to provide native HDMI 2.0 output. +- Supported Intel Platform: Skylake, Kaby Lake, Coffee Lake and later. +Skylake: Intel NUC Skull Canyon; Iris Pro 580 + HDMI 2.0 with Parade PS175 LSPCON. +Coffee Lake: Some laptops, Dell XPS 15 9570 for example, are equipped with HDMI 2.0 and Parade PS175 LSPCON. +- If you have confirmed that your HDMI 2.0 is routed to Intel IGPU and is working properly right now, you don't need to enable this driver, because your onboard LSPCON might already be configured in the firmware to work in PCON mode. + +#### Instructions +- Add the `enable-lspcon-support` property to `IGPU` to enable the driver, or use the boot-arg `-igfxlspcon` instead. +- Next, you need to know the corresponding connector index (one of 0,1,2,3) of your HDMI port. You could find it under IGPU in IORegistryExplorer. +- Add the `framebuffer-conX-has-lspcon` property to `IGPU` to inform the driver which connector has an onboard LSPCON adapter. +Replace `X` with the index you have found in the previous step. +The value must be of type `Data` and must be one of `01000000` (True) and `00000000` (False). +- (*Optional*) Add the `framebuffer-conX-preferred-lspcon-mode` property to `IGPU` to specify a mode for your onboard LSPCON adapter. +The value must be of type `Data` and must be one of `01000000` (PCON, DP to HDMI 2.0) and `00000000` (LS, DP to HDMI 1.4). +Any other invalid values are treated as PCON mode. +If this property is not specified, the driver assumes that PCON mode is preferred. +![](Img/lspcon.png) + +#### Debugging +Once you have completed the steps above, rebuild the kext cache and reboot your computer. +After plugging into your HDMI 2.0 cable (and the HDMI 2.0 monitor), you should be able to see the output on your monitor. +Dump your kernel log, and you should also be able to see something simillar to lines below. +``` +// When you plug the HDMI 2.0 cable +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a868c000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] No LSPCON chip associated with this framebuffer. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Returns 0x0. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a869a000. +igfx @ (DBG) SC: LSPCON::probe() DInfo: [FB2] Found the LSPCON adapter: Parade PS1750. +igfx @ (DBG) SC: LSPCON::probe() DInfo: [FB2] The current adapter mode is Level Shifter (DP++ to HDMI 1.4). +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver has detected the onboard chip successfully. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver has been initialized successfully. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x00. +igfx @ (DBG) SC: LSPCON::getMode() DInfo: [FB2] The current mode value is 0x01. +igfx @ (DBG) SC: LSPCON::setMode() DInfo: [FB2] The new mode is now effective. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] The adapter is running in preferred mode [Protocol Converter (DP++ to HDMI 2.0)]. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. + +// When you remove the HDMI 2.0 cable +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a868c000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] No LSPCON chip associated with this framebuffer. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Returns 0x0. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a869a000. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] LSPCON driver (at 0xffffff802ba3afe0) has already been initialized for this framebuffer. +igfx @ (DBG) SC: LSPCON::setModeIfNecessary() DInfo: [FB2] The adapter is already running in Protocol Converter (DP++ to HDMI 2.0) mode. No need to update. +igfx @ (DBG) SC: LSPCON::wakeUpNativeAUX() DInfo: [FB2] The native AUX channel is up. DPCD Rev = 0x12. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. +igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. +``` + +Additionally, you can find these properties injected by the driver under the corresponding framebuffer. (Only available in DEBUG version) +`fw-framebuffer-has-lspcon` indicates whether the onboard LSPCON adapter exists or not. +`fw-framebuffer-preferred-lspcon-mode` indicates the preferred adapter mode. 1 is PCON, and 0 is LS. +![](Img/lspcon_debug.png) + ## Known Issues *Compatibility*: - Limited cards: HD2000, HD2500 can only be used for [IQSV](https://www.applelife.ru/threads/zavod-intel-quick-sync-video.817923/) (they are used in real Macs only for this), there are no solutions. diff --git a/Manual/Img/lspcon.png b/Manual/Img/lspcon.png new file mode 100644 index 0000000000000000000000000000000000000000..f201a8fb1ed6d8de431105814e1a9bc989acf1bd GIT binary patch literal 28150 zcmZ6SWmsEVv}hB8JH;tZafjj#1zOzQrMMTj;847HaavqMad(&E?(XjPa?ZW)+8;j1 zPBx3ZWX?I}AVNt&8V#8U82|vFefj)J1pt77K)-iIgol3B7Hlj40ALBNBqWq9N}gc3#t!esg}QKKbje9WzB z@?Tluwom@Ty-2xlc6&OOIylD5+5D2LD5>PbewdEa(_RlIr@O&e60TuQLZ)7vCZ`jw zxerp>k1RM!6W~ms)?KhTaCCB3c@qSh?<8y4fgvEvnF~$Xl z<>15TYATa3>KDtwa4Vv?*XpiGba@*t%&fd-m)A>?;F3F$nf%!;LJ5xro)@YQy8*o( zIy~F z=cgnF;r0ZD`{zoD-R^C^LY8GPL4?*sJ74@+e*8k$W!=)ZE7%{GMaActCX>ZGdJIs> zdHeJy(ivQ-4PUAp6|K0)+<18Opxir#@00Bl!d)8R--h-F363t6Evyw;UJHa}Mkflt zhBt3n9E1+ffm4n{_}wt~EB^bv4#7+QnzYf*wywn2fdRvyhla5l2D32XK)h2;7o%?l0nyclyk8QPF+|`Nf;J5J3C(?Q&dP%OFF19r>r2LWx)AlN_ z^#!#ZSkR@)Qs5q~A2bLRqCb^g2VIBNFtyV9(!KPxj`|Myj{1)IcIv~#3k+v&wQaCC zlDQc6-CAD}>8yFS_m$iS#5I4)VNhV%Sk1B+Q}`xz+9(G_P_glN5JU ztT0stRI!U8(_$E5e+t}k>|marNoI=et5pssO;>$kw5Ry(kI+VO-DM{eOo%GE8Ipe< z?uQo#iH z{ed~zKI$MnlrsQ86!7Jfn3@OhBoi(JcLu*7gM@NYrK==7;`8_LK>6^@;1y;9I{5FD zfhNOiQgFxURUj-}HYM>-`EZ{wso^r-9t>`U!xrvVk6CIR?lQ(L+;>M$I63!T6_y2h zVW`6qg&K~NAq$!qk#UT1Ua5VbXKO6SJg0y}m5*G0)-5cS*R(-|d) z+WUh4@j7$}qQ)CwZ4;E?%o?Zv9Mpe&kWQSK4O2JRz7B2Xzi#uNZx5L7a;CK-hW_;5 zzvDY5f{gtbp%Vu3^c2#_KJ(!fdwOi5gy4|Pnf4iBJdOsbi@zJpZK*(jCSo(Ef zD|Xm(FS9;ZUB~m^5yx4e``oR#bG#m1M6{FnKP%_06#{YMEf%WHJxfH|cP^(thnl}C ziF7$$exXI`f1dmufxQ!Sx2CB5JZI>igtVskIjt=hY0c-Ud?ZtVXKjYbuuV@YmXujG znQ73SBt36^GGA7|{gpvf%UCsP>Tu*e0(U8Oa>YeRvY0!XbL*#lzsJEsXVq+B6`69# z%hTPs?ir8r>0Y+KflM+3_0vgxc82p|@gzjGP=5M{y3M~ctm3kMJ*AM4gsjr7CS^{| zanSeP;hRBOc7cLxXn-`S=cn5h_cg~^;yt?k^4o>}!Mc{jVP$2Cmh*$c>>&vZp$|*; z&E*0d2yBKsA-kjLw44@Wi9e`$o~93*B1|sp1ILBla}CMXb5fpZ=-TaMt$fF2{QAki zJRv;{^!~a)=yLzpWr7a8%7lWz8Iwh}>#WxxwQOSC<<3yE4RNf{tybexMYGv%6yfP& zFTVbVX6x`y)R&jhIm6-Rv(~-fuA4c1TGawMC-G*xW;bWMGaG%OyEXszku*-}I*qyh zr-j8ccAY&*omkq(hegIp-cjfJ759{*nu?RUB^E1hF6(K`j}s{dUSD371C$CCNFMGJ z6t|VjL65pCOupYP2}Jx_66sa@kQz%X8>I;5MBbjMt`V4hCh{chiR+iWN~h}-8TeNh zM4$_R{ET>i8uDv-`tGadd3|I1VDniUBL?HVPL)vrK=1u=O@iP>4|a9)P#rHau3gPp5P__S3opUD~{y>n|m_rofQ;l^}=>{|O=%#r+4^k^N(0|oy;ng%B z2sU?HeSWAuVrl-IFO!s^>HK}HR5pbP z|AXYuT7cDMW)Y1?(%_q_P|YztY7ol$$zYSo95NCDH(kbh(XW`qck~|>DiTPgE!zdb<$stF zp;PcjFh}%U2zOdOoeO)@8&LpIWZ zR%}Wp5%)Kk{bJheZwKI^W&||nz@6U-ipkl9@(I|Af)fGC9G{j1?$?7d@mho0)JD^J z+yP!c+H$g*+>dq_#pi2hL})^?QVdUjgrO2rDvI1Be&c_3cfF(B^t$>9oMjE?eyhmD7-ml z5kD@YiP8ZJQXwgz_hu302IWmte!>s;Tg1d&<&s{BebMh}rOAA2nxpFY4GjmJOT->+ zKAemDoo|Gl=KG18sz9vKNx0>Ha$8kb@Xy?m`X94*x`J@0+7S>i++ecSq>HO0b6|9` z)r^ot`%Hi&Zu7>@Jy_BMN60?S4CEnQCl^lv2&8c|^65ij$d>KTm$EE;M0_T}a_T3w z4r^^=fg`=uJ41t*wSmvjn8G0ge3;dBV74f}N$a&{zFpN_)s(G{|2JyRrxJ@QOVMyx zo#ksZkbOjVfeew0THKwk#dzy-A->?`n z0qBIv$#KZ9!GYsD$Q4z~`sFg77D?GRM{_CAEvReXumLaTi7>G2GbImm4nmB1eRd(t z$wl!Pt*S9*x>qviUoDkTho>Sl^uAF)SZS_LNb3=3dzsaBK6dK2ljGiLsxa;jqh(6Q zff=~{TT7$`?;_E|P&v`7BT&kP8<+fE90=&(Qj5*6|CGn{*Ud(sag(_yJN99j9%b|= zx2}b`Iw0IVwhgl1V4#s~V}J8==G5;b?*?ch%UB7l4VJ!d(O$q)%qQ{&~U5+lb;CZ26;#ZCms zE2^t8TCe2>y)~{vd^~Guigpn$HqUd6nLF&xit__SNW~HBWjx`&H^>UssznoGM$W>k zmufHrjUHCym>ViB?*TBzY1Yw*CN(~ar_@s)6*q35Yt5u9_5CXFFgtj!)2+KKy`~Bk zQ>GL}^jVtjO{_|t)SfJtwVOfcj^e`F`fj8Dra;>%pxUEpH_5$_GG{#xw|ma9nWyW& zg>ZjC#^m>hrIp)$>SNu={5ic-oXDG-QE=OE+aU7rT3@*xj$k>5e7uGD zvz7!zdX)DlTH&{wk*rOB5ygjAr!bk87ZaD>Z39hfPSobQAMi?CF-yP7k~r93s=6(# z$(n!H5st`QsaS3L%U$s&wd7Or5A@~B_gyfJwl__Z-T;-wu1i@uKu);^w}&2^e?X!J zC=LS&u>S~Wpk+3Al%1~cwlYH)u;1+r=NajWs&wwK9@F@8H}}I8r5ZRj^5-vdEoJ2l zpVQ7Iu1blC0;Vh(1m2r6F@Rmn^8`QlJt>_j5{qg@S=5Y0t4wXlSU9{A>p(M^C+R%< z8}YmmrYsX(vV4t`;`6Q-2W8eyjKG;(a$-YhWG6NY$d8Im$kRQrJv5zK4pHJVSW~k? ziS6hhCkWk{=+7I8{kWm5+jaJSzP*78VpX(v6afhnoZm-+7;(r&wbZ+V3lVKf5UPIX{_R)JfBJt)TQNqcNOa zLJN;RY%?xjL#0DKF;(QSSb#+HJ~p9nfOG)`QWXq6p?!q#d0ph_;hHfwkc0mMWgyiW zO5RAH^fb@x#R!*i}5_o{Q5X&T>EdS*-l6`_EujIVKo)9hvS$UyRXC0=z3}owmZg#4yYy{#k3A zOEZ8YSp~rMsf8I(*9K#Y=XX-(;@0@r*EQ|A{n)!f1C1vu+1fIK_AsovhGlJyi3EFg zT+P%Fax4<`&K#fFVEZwx&`+67{0Q&Q%PC$`?P_6d&BRJbL+qcgW8b%HvO^+3@8Np7 zF!7wQf)dhu^*oK&;|~oO3gG66LR-xyDhy^bLAR4h@xrhHaX>r@xjC&5msm{Wd5j;8 z>WB#0oqxONZ}2xBPs-}I*yevf(sk(eHIZbVPU^~3xu?DfpNzd))4(Yt zYT}+X80Dw!+mgKt0IxGDM=Zxv47*}@Ec%~XQPhtYuc^t{~TQa`?DL5DGSndC(+@^R4Yqv^Vf8piv#$|T+7gw2) z>_jg=D<)0fCMOs}+RsHOCN&HY_*&UCCZGlPfEFhw)%%^Zw0J^V^CE>oRfp!VMXGrO zw};I1p1duMFPaYnycZz|P<87rR`(_e@8yWxNwA=l18CyO1D$PVb@2b_$n3G1e4&rs zN!be_(4ez5-{D!(VaGH1nX>;Z8&1kzm6d9W6hTgt`jEzVB@o?RAX3}{sp-^{Bj&zg zc{y^pJKoMWYWakM&Rbn=)j1T)WhEo1e--R`jEyL zp9uM^WJ^yuv0_;axzRQpz6m(tFrDI9_aDfR1>xV${P{|{xiC5;8&40%deJA`$9 z=`;LwCxr5Bwvfs`za72HHqy(lR;vd;xuwK}G}eh0g(G1oHZzbyjBMCjVlg#LTe6E0 zP4OYiQ(V(Pw&|;o zOIswJ9dg;POx|*I^?^j)vj}asT=Ire%i5M@u4;5rwT3n9A0_p^Hd~7}$5P1f;4Dh4 zU;U6mLi3#L`7&l6h(gVjI1p(Q<*dul#<*zsWCyK^&1ewjQm-OV?eB?7-KSN` z=xK7Smz+`ZmDf0-2G-_d4nAqI?PQv^>0SkH0!nl>)4pWNvfsAmy@KyDgQ|wAss54< zDKhOpP@i(zNIv@v2!3fkCdZ<_e7zbkfwhb&_*L{=-jVEmb`-h0!81OwRx+PMyk2hR zo?X<-&H5rU3x9Q0l2p3vm9)Q{!PKrlY>RG&8ZO_v%N8!5)L-%b-9yi{wK(hdIc=Tm3XJ9r3Osl{x%+)jrS3hlfKDtQ&LP0)XY1UB@tEx>Gz{-v{*0u6;g^rDRey^>5N&%8u(Ra2PPJm?Z zut9o6GZsvAXkr8lQ|#2%&#r z+BgsdN%P0BgZfZ|GaTnR-YP|JR4k zZ2wn6fhdh~9k>)jzs6zr71z0P&DcsFMn*>7yJZ&;E-d6`hel$p%{K@`Tkh$i)>$*7Z$G==LwWGzo&A>w?W$M4?nnf z=R?s+h=_g8I|Kaqr=rh>7%Ne)y*DH3@@R`autxD;giQNT?@&9tU+&gK8V(B5-`y2h zjAyYh>-70S@f-p|Uo06}k@(O1^p935n+w+`RHXyI*7ELBoLxs88uiq(a?_+@h!4!z z`dcuakv#8TpUw~lx}orp-TWc~n-=~tZ6)#G{ZH`m8Z6TMymx59V$y41TXVJj z9ynrteH1J5I*&7>n@KnX9Q^GE8dBifNi>2o$5V&bXU|T~aY1Z=AZzRO|1~6N5Te6ENYXMsypc;4`P`gKevF;~v z|JLEz6OLt19JLpOA^bVn^$nos^*l)0v}7vC@gltFU@3VixAE{vjP<39M_#nz$yrS1rnll3}bSet_YI5)%T zk2EuHC`k52*k7u#!ftsyEbaUiD@=4;Go^rJz6V8n?(K~{QhfoVqXF<}L=ATxj0?o^ z_(ERSj^ivZUkc<34lT5b$7CDTZg2~Z29~=qL=TU6%X}7SK@=JBF5<#NZuEGvagN`8;yGbJqz;{QPGsAvy3o=NK=Q z&tN8x)#L}>&2Xw^zMSpvj1`DPJod)`l6AM;ku(;xjqRgra&8^MMN(`y4Uv85vPSZ{ zIovsfi6iyBo&bsus5V{yis5G{(yYunLYWmpw4vBK*O+%1 zAdq}hI)?%|+8^VBS0eyNnPxC^-ysr(%v!ehDW5wMiUTz1%2c}X+#Sn2dJG2NK*x$# zz}j}-HFI^|jnO=8c@q_KLFPO_(b*9IT5$z96e?uGpj|n(UvY3V?UC?*LnLS~>?iXV zRGRY@eL8RVyxJ8WBuO*Usk3cd<$9wcVO_`SSkW!^f4R||8-p&BQ&?d^P?UGmarH1v z2WSZG6WKvZypxfO3wZCm3x!pVE={#i0qwNy$sWP1+3n=830s|J>V~~cZ7*N1i62FR zXjT?yS1bsgZ($^UO)r)zie4FI13H~&U|Wt3TsX*p^5O-alo?g^L+jlt3KV*ufZ2Uh zg?*cb3v&@8Sb#t2hg*Z=?8tntl|?fLY8;=g6IuqiDokfA#yY&B0{Xowg$;-;jq% zkOSn&<@;}x?+mk%*_36!<3NF2l4iL+PH?l!TctuKKg-voOJq06&fND~P%636*WEzy z$9g5N!rxGC`s8%1P%dpV201i@y%VJP58RwCQ6KJwJlbqfJw#ZA;8cA_mgc*iHoq=sKJmN_Jm*oa zxerZJ?ct^hc)#*H1esYsj)>!gM+RzZW``n%D$D@q5dwG8XMja&t!YG?B_12YM z(j2xr&NcQujXe26Ct{DTUmzE`Ist9q!z%uz#B_14&?WSo4QEIQv=!@9=}QQGYjC@p zH^orS*Hojw`H}|@t9*X)IM!T1$|a@|_C=y~8@g983>R(pK=7!TXgn^y;@nd_W~B_r z5MQmPZ)sJ+zhsgaB3gP9DE|n)y+ew}-lZ}_;Yhngp8gc@GeKm@p}kf2EElYd$CP@u zc^O}OwKuLf9jK=fBxOQ^aHTr>lPlk(_j|R+g$Z*#dA$4Xh&BuB_>WM525-%B9WG-( zm96McbJ9vmV)+SYQN7rvl#O%bz}u1Qi)tpf%9 z^HngWiLqL$N2B_aD=WoL8s^7SO*I#{z+31!obXS;!7Vmu^~$4zfi>``>V>J}FdquH zUVv6_I(_&wwV#KeL1Nzn%qA8GCH!jD)zxO_>zy&>a?8S&ZU6Y~cv&2U|-x>s`Q)tDX)s5`v-p7-rSt); z+_{2!iQ1r0GgHsCjH?!sBt?M&9tcPSl!11cQoZkhzXo6d@BpF^(uJH^=|*Va$@~$k zx4Qz1sA5oVl$VR5pg}pVMCmysEy3Nj)hghqY;;Uj3zfi}xy6VI4=U9gOt z3PUNcd!1Rmf8Fz&6U1=;r}b(sO9wGIYDY*^z&f@aC+ov28@^n&aQC3$wg4E*6vkIV z3g||G7fJD?of;VrSQyVh6utHI6&P_R5PEi4++N6uX~A^)(DCyj4@&DR=w7}OkJu0( z=6%v7a3w&2ePi1#nwuOI^E|+}V<($JA0pl|Ta};yOA~z4Ld_b|!aGTS852aeRmMbV z{(A`7wZila^tMz#9QyK%o=r}S*@ejp!eE#Mk?Oh9^OnnQy~mcUqVYh{+cen^ZkYy6 zAi%;iK4IY?+SN2W`nDJ$>VMparY8CF7+hNlSeDc2`Pg9EV0gRW2z+b`SSzjIKMI1E z161}1fHFeNWN|tjyhzcR5_Nr|H%z9iddqP+Ya-J4&wH@vg)FY_Xq|tq#snfm?Hbn3 z!4adnKN|O$o0oYM9|+$EO0Kxss}S?p>%nL}^7C;tvQ4mW7(gRZUd4-JwZJ$UYemx` z!<@t8*r!NWJn;@m&waQ`gx=LIk~-^#uG?|mst{f!Xp%U`K~62FDEwe1laH-Lq*2m+ z#<~L#g(kqx;(U@Uc|9=bg1M-x9$V3;vAN5ko>@pN6M$6!pwnJSuRgM1#!$ z)+LBd3Gt$1J)(0Td;ug3WLo=gqzX^~cd-L`+UAC&O>bLiejK8bcE~e9p(8mA1l+gq2Cymik95pW1;x9SkN#M zK+qP6h75c&Koxr}IV&)U&y?hpxCWWvVLkt)+qDesiI2e{?SGgiA4zcZckV{%XhkSQ zR8~Y(Vro4f)l~$%EK_v|Zo^gV{bIF|-s(u+?6}-v)SH@cr_7v8tEdFiWVy6RWqF(d zAX z5`eQ%$n7i6l7kv{1v3!O7mWLXN|ZEN+DwF$I|>)`3UtXK|I=6mZaHRjCPeU&jj zJ~3|ms&mgbXw%;1>hy-F-kFYAL?alu7sp1x`sADL#w18gXMh>nWl%}#;k51UH_)v9 z_fiNLgVkb?a5JY$q9}L)n}Xvuh52h6i3dsNTe6O-uXpjJFQNY@@RiBaJT$pcm+~M7 zP{cI23WCKu*x_mLKk7NCOLc$p$ot|=*#igwKP_Vij_STkqx&YnlATye6^O}98n74P zA-EaTotO<_*XV-9%#-WW(rcX|7{=7Fm^6g;NfQCd!rP(vT0Teuq63|@T~y>`Ljk(8 z*t3*%SOSbu zn?4WFw`$q`%E1f~k~>a_oxy_hM!z^`CmIQgH16X z`+YyCbrzjF=ZJ!}=Dk~3HaBzkQVln$Vf0|Yj*%4mkVTj(LGp;wX7LbL4QU!sFzPIv zo*7s(IN1JX`{NfqBd3F!kb-dPpm~Zh&xLMsygNde&{SPKMa-~}I3s0TR&o^7zdxMd zs`mpzY@^UNI%U5%XKHcOC~OKIR3dc9p(AP-SOeJB4cxKAO*_2~dWwvdjhu*C{K3=| z_yz~(X;=K*LmP`d&Yw~NHAE};3cbdK3*s*h@;=4ADS{v@$QY)jr((ZM%#9NiqYw`3bqj`x0l=9*oJ-z>lWesTF zE}x(&uT@h>vTjc|GzFmqGj9Nu8f@{YPrt5Y4K?;@;dH_vwG$wVe(?n}(y?UD zbTa82&Kwcd>!kE5ZG_k|6?BBaoWnDLwzY*~hqIfo(1Qg5J=&jgb;EdbF~~u%CNJmg z7Km(z=Xi1FEO0duai70N$oHc3At)heaenO*tb)ZnbAxhofk<{wpNEzxv|J?2UXH_Z z5-8h9ppXdtAj(HB*dhV6GiIg~S;s#@gNb*_-+~**^<1YKhsDE7L@)``zAn^_XoNGA zYl(teJxaFxxQEy|Y8Ed;_vGXD5#1IUJ?B%dj~D=bOt~nYP+1&engU2v$Z}ZuItY}- zk)$hA3md*v07WSWUxXXAirYhs5PIkP1ww~|$0NuPJ_E^>q

Uwu?KMNPjVL@vmDM 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 Z;FBF^9h(>d@H{urD`|PDA_;x3{{u@A@{Rxi literal 0 HcmV?d00001 diff --git a/Manual/Img/lspcon_debug.png b/Manual/Img/lspcon_debug.png new file mode 100644 index 0000000000000000000000000000000000000000..ac14b14fce42b5b86ede0c58da5885309fee08d0 GIT binary patch literal 93760 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 Date: Sat, 22 Jun 2019 17:22:11 +0800 Subject: [PATCH 013/102] Improve the return type of AppleIntelFramebufferController::ReadAUX(). --- WhateverGreen/kern_igfx.cpp | 4 ++-- WhateverGreen/kern_igfx.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 5e4f4a8e..8a557dcc 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -646,7 +646,7 @@ bool IGFX::wrapAcceleratorStart(IOService *that, IOService *provider) { /** * ReadAUX wrapper to modify the maximum link rate valud in the DPCD buffer */ -int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { +IOReturn IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { // // Abstract: @@ -667,7 +667,7 @@ int IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ // Call the original ReadAUX() function to read from DPCD - int retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); + IOReturn retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); // Guard: Check the DPCD register address // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index a57095a5..56bb33be 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -250,7 +250,7 @@ class IGFX { /** * Original AppleIntelFramebufferController::ReadAUX function */ - int (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; + IOReturn (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; /** * Original AppleIntelFramebufferController::ReadI2COverAUX function @@ -482,7 +482,7 @@ class IGFX { /** * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer */ - static int wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); + static IOReturn wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); /** * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 From c6309d66f743c7c97e2c9d5655861fdf61caaa14 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 21:48:58 +0800 Subject: [PATCH 014/102] Fix the infinite loop when calculating dividers for HDMI connection on SKL, KBL and CFL platforms. --- WhateverGreen/kern_igfx.cpp | 208 +++++++++++++++++++++++++++++++++++- WhateverGreen/kern_igfx.hpp | 120 +++++++++++++++++++++ 2 files changed, 327 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 8a557dcc..86e8b134 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -165,6 +165,9 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { dumpPlatformTable = true; #endif + // Enable the fix for computing HDMI dividers on SKL, KBL, CFL platforms if the corresponding boot argument is found + hdmiP0P1P2Patch = checkKernelArgument("-igfxhdmidivs"); + // Enable the LSPCON driver support if the corresponding boot argument is found supportLSPCON = checkKernelArgument("-igfxlspcon"); // Or if "enable-lspcon-support" is set in IGPU property @@ -256,7 +259,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { hdmiAutopatch = !applyFramebufferPatch && !connectorLessFrame && getKernelVersion() >= Yosemite && !checkKernelArgument("-igfxnohdmi"); // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch && !supportLSPCON; + switchOffFramebuffer = !blackScreenPatch && !applyFramebufferPatch && !dumpFramebufferToDisk && !dumpPlatformTable && !hdmiAutopatch && cflBacklightPatch == CoffeeBacklightPatch::Off && !maxLinkRatePatch && !hdmiP0P1P2Patch && !supportLSPCON; switchOffGraphics = !pavpDisablePatch && !forceOpenGL && !moderniseAccelerator && !avoidFirmwareLoading && !readDescriptorPatch; } else { switchOffGraphics = switchOffFramebuffer = true; @@ -405,6 +408,22 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } } + if (hdmiP0P1P2Patch) { + auto ppp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController17ComputeHdmiP0P1P2EjP21AppleIntelDisplayPathPNS_10CRTCParamsE", address, size); + if (ppp) { + patcher.eraseCoverageInstPrefix(ppp); + orgComputeHdmiP0P1P2 = reinterpret_cast(patcher.routeFunction(ppp, reinterpret_cast(wrapComputeHdmiP0P1P2), true)); + if (orgComputeHdmiP0P1P2) { + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() has been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "SC: Failed to route ComputeHdmiP0P1P2()."); + } + } else { + SYSLOG("igfx", "SC: Failed to find ComputeHdmiP0P1P2()."); + } + } + if (supportLSPCON) { auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); @@ -702,6 +721,193 @@ IOReturn IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t ad return retVal; } +void IGFX::populateP0P1P2(struct ProbeContext *context) { + uint32_t p = context->divider; + uint32_t p0 = 0; + uint32_t p1 = 0; + uint32_t p2 = 0; + + // Even divider + if (p % 2 == 0) { + unsigned int half = p / 2; + if (half == 1 || half == 2 || half == 3 || half == 5) { + p0 = 2; + p1 = 1; + p2 = half; + } else if (half % 2 == 0) { + p0 = 2; + p1 = half / 2; + p2 = 2; + } else if (half % 3 == 0) { + p0 = 3; + p1 = half / 3; + p2 = 2; + } else if (half % 7 == 0) { + p0 = 7; + p1 = half / 7; + p2 = 2; + } + } + // Odd divider + else if (p == 3 || p == 9) { + p0 = 3; + p1 = 1; + p2 = p / 3; + } else if (p == 5 || p == 7) { + p0 = p; + p1 = 1; + p2 = 1; + } else if (p == 15) { + p0 = 3; + p1 = 1; + p2 = 5; + } else if (p == 21) { + p0 = 7; + p1 = 1; + p2 = 3; + } else if (p == 35) { + p0 = 7; + p1 = 1; + p2 = 5; + } + + context->pdiv = p0; + context->qdiv = p1; + context->kdiv = p2; +} + +int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) +{ + // + // Abstract + // + // ComputeHdmiP0P1P2 is used to compute required parameters to establish the HDMI connection. + // Apple's original implementation cannot find appropriate dividers for a higher pixel clock + // value like 533.25 MHz (HDMI 2.0 4K @ 60Hz) and then causes an infinite loop in the kernel. + // + // This reverse engineering research focuses on identifying fields in CRTC parameters by + // carefully analyzing Apple's original implementation, and a better implementation that + // conforms to Intel Graphics Programmer Reference Manual is provided to fix the issue. + // + // This is the first stage to try to solve the HDMI output issue on Dell XPS 15 9570, + // and is now succeeded by the LSPCON driver solution. + // When the onboard LSPCON chip is running in LS mode, macOS recognizes the HDMI port as + // a HDMI port. Consequently, ComputeHdmiP0P1P2() is called by SetupClock() to compute + // the parameters for the connection. + // In comparison, when the chip is running in PCON mode, macOS recognizes the HDMI port as + // a DisplayPort port. As a result, this method is never called by SetupClock(). + // + // This fix is left here as an emergency fallback and for reference purposes, + // and is compatible for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + // + // It is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. + // + // - FireWolf + // - 2019.06 + // + + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Called with pixel clock = %d Hz.", pixelClock); + + /// All possible dividers + static constexpr uint32_t dividers[] = { + // Even dividers + 4, 6, 8, 10, 12, 14, 16, 18, 20, + 24, 28, 30, 32, 36, 40, 42, 44, 48, + 52, 54, 56, 60, 64, 66, 68, 70, 72, + 76, 78, 80, 84, 88, 90, 92, 96, 98, + + // Odd dividers + 3, 5, 7, 9, 15, 21, 35 + }; + + /// All possible central frequency values + static constexpr uint64_t centralFrequencies[3] = {8400000000ULL, 9000000000ULL, 9600000000ULL}; + + // Calculate the AFE clock + uint64_t afeClock = pixelClock * 5; + + // Prepare the context for probing P0, P1 and P2 + struct ProbeContext context; + bzero(&context, sizeof(struct ProbeContext)); + + // Apple chooses 400 as the initial minimum deviation + // However 400 is too small for a pixel clock like 533.25 MHz (HDMI 2.0 4K @ 60Hz) + // Raise the value to UInt64 MAX + // It's OK because the deviation is still bound by MAX_POS_DEV and MAX_NEG_DEV. + context.minDeviation = UINT64_MAX; + + for (int dindex = 0; dindex < sizeof(dividers) / sizeof(int); dindex++) { + for (int cfindex = 0; cfindex < sizeof(centralFrequencies) / sizeof(uint64_t); cfindex++) { + int divider = dividers[dindex]; + uint64_t central = centralFrequencies[cfindex]; + // Calculate the current DCO frequency + uint64_t frequency = divider * afeClock; + // Calculate the deviation + uint64_t deviation = (frequency > central ? frequency - central : central - frequency) * 10000 / central; + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Dev = %6llu; Central = %10llu Hz; DCO Freq = %12llu Hz; Divider = %2d.\n", deviation, central, frequency, divider); + + // Guard: Positive deviation is within the allowed range + if (frequency >= central && deviation >= SKL_DCO_MAX_POS_DEVIATION) + continue; + + // Guard: Negative deviation is within the allowed range + if (frequency < central && deviation >= SKL_DCO_MAX_NEG_DEVIATION) + continue; + + // Guard: Less than the current minimum deviation value + if (deviation >= context.minDeviation) + continue; + + // Found a better one + // Update the value + context.minDeviation = deviation; + context.central = central; + context.frequency = frequency; + context.divider = divider; + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: FOUND: Min Dev = %8llu; Central = %10llu Hz; Freq = %12llu Hz; Divider = %d\n", deviation, central, frequency, divider); + + // Guard: Check whether the new minmimum deviation has been reduced to 0 + if (deviation != 0) + continue; + + // Guard: An even divider is preferred + if (divider % 2 == 0) { + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Found an even divider [%d] with deviation 0.\n", divider); + break; + } + } + } + + // Guard: A valid divider has been found + if (context.divider == 0) { + SYSLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: Cannot find a valid divider for the given pixel clock %d Hz.\n", pixelClock); + return 0; + } + + // Calculate the p,q,k dividers + populateP0P1P2(&context); + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Divider = %d --> P0 = %d; P1 = %d; P2 = %d.\n", context.divider, context.pdiv, context.qdiv, context.kdiv); + + // Calculate the CRTC parameters + uint32_t multiplier = (uint32_t) (context.frequency / 24000000); + uint32_t fraction = (uint32_t) (context.frequency - multiplier * 24000000); + uint32_t cf15625 = (uint32_t) (context.central / 15625); + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Multiplier = %d; Fraction = %d; CF15625 = %d.\n", multiplier, fraction, cf15625); + auto params = reinterpret_cast(parameters); + if (params == nullptr) { + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: Failed to cast the given CRTC parameter pointer."); + return 0; + } + params->pdiv = context.pdiv; + params->qdiv = context.qdiv; + params->kdiv = context.kdiv; + params->multiplier = multiplier; + params->fraction = fraction; + params->cf15625 = cf15625; + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: CTRC parameters have been populated successfully."); + return 0; +} + IGFX::LSPCON::LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath) { this->controller = controller; this->framebuffer = framebuffer; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 56bb33be..b01bfdc6 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -266,6 +266,11 @@ class IGFX { * Original AppleIntelFramebufferController::GetDPCDInfo function */ IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *); + + /** + * Original AppleIntelFramebufferController::ComputeHdmiP0P1P2 function + */ + int (*orgComputeHdmiP0P1P2)(void *, uint32_t, void *, void *); /** * Detected CPU generation of the host system @@ -309,6 +314,11 @@ class IGFX { */ bool verboseI2C {false}; + /** + * Set to true to fix the infinite loop issue when computing dividers for HDMI connections + */ + bool hdmiP0P1P2Patch {false}; + /** * Set to true if PAVP code should be disabled */ @@ -484,6 +494,116 @@ class IGFX { */ static IOReturn wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); + /** + * Reflect the `AppleIntelFramebufferController::CRTCParams` struct + * + * @note Unlike the Intel Linux Graphics Driver, + * - Apple does not transform the `pdiv`, `qdiv` and `kdiv` fields. + * - Apple records the final central frequency divided by 15625. + * @ref static void skl_wrpll_params_populate(params:afe_clock:central_freq:p0:p1:p2:) + * @seealso https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1171 + */ + struct CRTCParams + { + /// Uninvestigated fields + uint8_t uninvestigated[32]; + + /// P0 [`CRTCParams` field offset 0x20] + uint32_t pdiv; + + /// P1 [`CRTCParams` field offset 0x24] + uint32_t qdiv; + + /// P2 [`CRTCParams` field offset 0x28] + uint32_t kdiv; + + /// Difference in Hz [`CRTCParams` field offset 0x2C] + uint32_t fraction; + + /// Multiplier of 24 MHz [`CRTCParams` field offset 0x30] + uint32_t multiplier; + + /// Central Frequency / 15625 [`CRTCParams` field offset 0x34] + uint32_t cf15625; + + /// Other fields that are not of interest + uint8_t others[0]; + }; + + /** + * Represents the current context of probing dividers for HDMI connections + */ + struct ProbeContext + { + /// The current minimum deviation + uint64_t minDeviation; + + /// The current chosen central frequency + uint64_t central; + + /// The current DCO frequency + uint64_t frequency; + + /// The current selected divider + uint32_t divider; + + /// The corresponding pdiv value [P0] + uint32_t pdiv; + + /// The corresponding qdiv value [P1] + uint32_t qdiv; + + /// The corresponding kqiv value [P2] + uint32_t kdiv; + }; + + /** + * The maximum positive deviation from the DCO central frequency + * + * @note DCO frequency must be within +1% of the DCO central frequency. + * @warning This is a hardware requirement. + * See "Intel Graphics Programmer Reference Manual for Kaby Lake platform" + * Volume 12 Display, Page 134, Formula for HDMI and DVI DPLL Programming + * @link https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf + * @note This value is appropriate for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1080 + */ + static constexpr uint64_t SKL_DCO_MAX_POS_DEVIATION = 100; + + /** + * The maximum negative deviation from the DCO central frequency + * + * @note DCO frequency must be within -6% of the DCO central frequency. + * @seealso See `SKL_DCO_MAX_POS_DEVIATION` above for details. + */ + static constexpr uint64_t SKL_DCO_MAX_NEG_DEVIATION = 600; + + /** + * [Helper] Compute the final P0, P1, P2 values based on the current frequency divider + * + * @param context The current context for probing P0, P1 and P2. + * @note Implementation adopted from the Intel Graphics Programmer Reference Manual; + * Volume 12 Display, Page 135, Algorithm to Find HDMI and DVI DPLL Programming. + * Volume 12 Display, Page 135, Pseudo-code for HDMI and DVI DPLL Programming. + * @ref static void skl_wrpll_get_multipliers(p:p0:p1:p2:) + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 + */ + static void populateP0P1P2(struct ProbeContext* context); + + /** + * Compute dividers for a HDMI connection with the given pixel clock + * + * @param that The hidden implicit `this` pointer + * @param pixelClock The pixel clock value (in Hz) used for the HDMI connection + * @param displayPath The corresponding display path + * @param parameters CRTC parameters populated on return + * @return Never used by its caller, so this method might return void. + * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` + */ + static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); + /** * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 * From f22f5e4c77d565f562885613ee75ec50684294e1 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 21:59:53 +0800 Subject: [PATCH 015/102] Update the README and change log for the infinite loop fix. --- Changelog.md | 1 + README.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 6d73649d..7c423c4b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ WhateverGreen Changelog - Disabled NVIDIA performance fix on 10.15, as it now is built-in - Enable HDMI 2.0 patches on 10.14+ (Use at own risk in case of undiscovered change) - Added CFL graphics kernel panic workaround on 10.14.4+ +- Added infinite loop fix when calculating dividers for Intel HDMI connections on SKL, KBL and CFL platforms. - Added driver support for onboard LSPCON chips to enable DisplayPort to HDMI 2.0 output on Intel IGPUs (by @0xFireWolf) #### v1.2.9 diff --git a/README.md b/README.md index 1a87ffbd..f56478f7 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ WhateverGreen - For Intel HD digital audio HDMI, DP, Digital DVI (Patches connector-type DP -> HDMI) - Fixes NVIDIA GPU interface stuttering on 10.13 (official and web drivers) - Fixes the kernel panic caused by an invalid link rate reported by DPCD on some laptops with Intel IGPU. +- Fixes the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on Skylake, Kaby Lake and Coffee Lake platforms. - Implements the driver support for onboard LSPCON chips to enable DisplayPort to HDMI 2.0 output on some platforms with Intel IGPU. #### Documentation @@ -56,6 +57,7 @@ Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) an - `igfxcflbklt=1` boot argument (and `enable-cfl-backlight-fix` property) to enable CFL backlight patch - `applbkl=0` boot argument to disable AppleBacklight.kext patches for IGPU. In case of custom AppleBacklight profile- [read here.](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.OldPlugins.en.md) - `-igfxmlr` boot argument (and `enable-dpcd-max-link-rate-fix` property) to apply the maximum link rate fix. +- `-igfxhdmidivs` boot argument to fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on SKL, KBL and CFL platforms. - `-igfxlspcon` boot argument (and `enable-lspcon-support` property) to enable the driver support for onboard LSPCON chips. [Read the manual](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.IntelHD.en.md) - `-igfxi2cdbg` boot argument to enable verbose output in I2C-over-AUX transactions (only for debugging purposes). @@ -63,7 +65,7 @@ Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) an - [Apple](https://www.apple.com) for macOS - [AMD](https://www.amd.com) for ATOM VBIOS parsing code - [The PCI ID Repository](http://pci-ids.ucw.cz) for multiple GPU model names -- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix and LSPCON driver support +- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix, infinite loop fix for Intel HDMI connections and LSPCON driver support - [Floris497](https://github.com/Floris497) for the CoreDisplay [patches](https://github.com/Floris497/mac-pixel-clock-patch-v2) - [Fraxul](https://github.com/Fraxul) for original CFL backlight patch - [headkaze](https://github.com/headkaze) for Intel framebuffer patching code and CFL backlight patch improvements From f4c5ff616a5b88bb161c85e780d8b14a5f3a9f45 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 22:38:07 +0800 Subject: [PATCH 016/102] Update the manual for the infinite loop fix and improve the comments. --- Manual/FAQ.IntelHD.cn.md | 12 +++++++++++- Manual/FAQ.IntelHD.en.md | 8 ++++++++ WhateverGreen/kern_igfx.cpp | 11 +++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 3ca8ac7e..c867887f 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1734,6 +1734,15 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest 可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 + +## 修复核显驱动在尝试点亮外接 HDMI 高分辨率显示器时造成的死循环问题 +**适用平台:** 第六代酷睿 Skylake 核显,第七代酷睿 Kaby Lake 核显以及第八代酷睿 Coffee Lake。 +使用 `-igfxhdmidivs` 启动参数以解决核显驱动试图点亮外接 HDMI 高分辨率显示器时造成的系统死机问题。 +具体症状表现在插入 HDMI 线后,笔记本内屏变黑但有背光,且系统无响应,外屏也无输出。 +#### 关于使用此修复补丁的一些建议 +- 如果你的笔记本或台式机主板有 HDMI 1.4 接口,并且想使用 2K 或 4K HDMI 显示器的话,你可能需要这个补丁。 +- 如果你的笔记本或台式机主板有 HDMI 2.0 接口,并且当前 HDMI 输出有问题,那么建议你启用 LSPCON 驱动支持以获得更好的 HDMI 2.0 体验。(详情请阅读下方 LSPCON 章节) + ## 启用 LSPCON 驱动以支持核显 DisplayPort 转 HDMI 2.0 输出 #### 简述 近几年的笔记本都开始配备了 HDMI 2.0 输出端口。这个端口可能直接连到核显上也有可能连在独显上。 @@ -1755,7 +1764,8 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest #### 如何使用 - 为核显添加 `enable-lspcon-support` 属性或者直接使用 `-igfxlspcon` 启动参数来启用驱动。 -- 接下来你需要知道 HDMI 2.0 对应的端口号是多少。这个你可以直接在 IORegistryExplorer 里看到。在 AppleIntelFramebuffer@0/1/2/3 下面找到你的外接显示器。 +- 接下来你需要知道 HDMI 2.0 对应的端口号是多少。这个你可以直接在 IORegistryExplorer 里看到。在 AppleIntelFramebuffer@0/1/2/3 下面找到你的外接显示器。 +*如果你身边只有 2K/4K HDMI 显示器的话,你需要先启用上面的死循环修复补丁,否则当你连接显示器时,系统直接死机,所以就看不到对应的端口号了。* - 为核显添加 `framebuffer-conX-has-lspcon` 属性来通知驱动哪个接口下面有 LSPCON 信号转换器。把 `conX` 里的 X 替换成你在上一步找到的端口值。 这个属性的对应值请设为 `Data` 类型。如果接口下存在转换器的话,请设为 `01000000`,反之设为 `00000000`。 若不定义这个属性的话,驱动默认认为对应接口下**不存在**转换器。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index a7574361..f49ad94a 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1665,6 +1665,13 @@ All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HB If an invalid value is specified, the default value `0x14` will be used instead. If this property is not specified, same as above. +## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on Skylake, Kaby Lake and Coffee Lake platforms +Add the `-igfxhdmidivs` boot argument to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. +#### General Notes +- For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their HDMI 1.4 port, you might find this fix helpful. +- For those who have a laptop or PC with HDMI 2.0 routed to Intel IGPU and have HDMI output issues, please note that this fix is now succeeded by the LSPCON driver solution, and it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. +*(You might still need this fix temporarily to figure out the framebuffer index of your HDMI port. See the LSPCON section below.)* + ## LSPCON driver support to enable DisplayPort to HDMI 2.0 output on Intel IGPU #### Brief Introduction Recent laptops (Kaby Lake/Coffee Lake-based) are typically equipped with a HDMI 2.0 port. This port could be either routed to IGPU or DGPU, and you can have a confirmation on Windows 10. Intel (U)HD Graphics, however, does not provide native HDMI 2.0 output, so in order to solve this issue OEMs add an additional hardware named LSPCON on the motherboard to convert DisplayPort into HDMI 2.0. @@ -1681,6 +1688,7 @@ Coffee Lake: Some laptops, Dell XPS 15 9570 for example, are equipped with HDMI #### Instructions - Add the `enable-lspcon-support` property to `IGPU` to enable the driver, or use the boot-arg `-igfxlspcon` instead. - Next, you need to know the corresponding connector index (one of 0,1,2,3) of your HDMI port. You could find it under IGPU in IORegistryExplorer. +*If you only have a 2K/4K HDMI monitor, you might need to enable the infinite loop fix before connecting a HDMI monitor to your build, otherwise the system just hangs and you won't be able to run the IORegistryExplorer and find the framebuffer index.* - Add the `framebuffer-conX-has-lspcon` property to `IGPU` to inform the driver which connector has an onboard LSPCON adapter. Replace `X` with the index you have found in the previous step. The value must be of type `Data` and must be one of `01000000` (True) and `00000000` (False). diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 86e8b134..691b9f4d 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -789,8 +789,9 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa // carefully analyzing Apple's original implementation, and a better implementation that // conforms to Intel Graphics Programmer Reference Manual is provided to fix the issue. // - // This is the first stage to try to solve the HDMI output issue on Dell XPS 15 9570, + // This is the first stage to try to solve the HDMI 2.0 output issue on Dell XPS 15 9570, // and is now succeeded by the LSPCON driver solution. + // LSPCON is used to convert DisplayPort signal to HDMI 2.0 signal. // When the onboard LSPCON chip is running in LS mode, macOS recognizes the HDMI port as // a HDMI port. Consequently, ComputeHdmiP0P1P2() is called by SetupClock() to compute // the parameters for the connection. @@ -799,8 +800,14 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa // // This fix is left here as an emergency fallback and for reference purposes, // and is compatible for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + // Note that it is still capable of finding appropriate dividers for a 1080p HDMI connection + // and is more robust than the original implementation. // - // It is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. + // For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their + // HDMI 1.4 port, you might find this fix helpful. + // + // For those who have a laptop or PC with HDMI 2.0 routed to IGPU and have HDMI output issues, + // it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. // // - FireWolf // - 2019.06 From 859ee414b86373533c3f702ac45a64c1af654a5a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 23:08:38 +0800 Subject: [PATCH 017/102] Improve the formatting in the manual --- Manual/FAQ.IntelHD.cn.md | 23 +++++++++++++---------- Manual/FAQ.IntelHD.en.md | 32 +++++++++++++++++++------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index c867887f..d18b3c7b 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1736,9 +1736,9 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复核显驱动在尝试点亮外接 HDMI 高分辨率显示器时造成的死循环问题 -**适用平台:** 第六代酷睿 Skylake 核显,第七代酷睿 Kaby Lake 核显以及第八代酷睿 Coffee Lake。 -使用 `-igfxhdmidivs` 启动参数以解决核显驱动试图点亮外接 HDMI 高分辨率显示器时造成的系统死机问题。 -具体症状表现在插入 HDMI 线后,笔记本内屏变黑但有背光,且系统无响应,外屏也无输出。 +**适用平台:** 第六代酷睿 Skylake 核显,第七代酷睿 Kaby Lake 核显以及第八代酷睿 Coffee Lake。 +使用 `-igfxhdmidivs` 启动参数以解决核显驱动在试图点亮外接 HDMI 高分辨率显示器时造成的系统死机问题。 +具体症状表现为插入 HDMI 线后,笔记本内屏变黑但有背光,系统无响应,并且外屏也无输出。 #### 关于使用此修复补丁的一些建议 - 如果你的笔记本或台式机主板有 HDMI 1.4 接口,并且想使用 2K 或 4K HDMI 显示器的话,你可能需要这个补丁。 - 如果你的笔记本或台式机主板有 HDMI 2.0 接口,并且当前 HDMI 输出有问题,那么建议你启用 LSPCON 驱动支持以获得更好的 HDMI 2.0 体验。(详情请阅读下方 LSPCON 章节) @@ -1750,7 +1750,7 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest 如果连在了核显上,那么笔记本厂商需要在主板上安装额外的信号转换器来把 DP 信号转换成 HDMI 2.0 信号, 这是因为现阶段英特尔的核显并不能原生提供 HDMI 2.0 信号输出。(类似主板厂商使用第三方芯片以提供 USB 3.0 功能) 这个信号转换器名为 LSPCON,全称 **L**evel **S**hifter and **P**rotocol **Con**verter,并且有两种工作模式。 -当转换器工作在 LS 模式下,它可以把 DP 转换成 HDMI 1.4 信号;在 PCON 模式下,它可以把 DP 转换成 HDMI 2.0 信号。 +当工作在 LS 模式下,它可以把 DP 转换成 HDMI 1.4 信号。在 PCON 模式下,它可以把 DP 转换成 HDMI 2.0 信号。 然而有些厂商在转换器的固件里把 LS 设为了默认的工作模式,这就导致在 macOS 下 HDMI 2.0 连接直接黑屏或者根本不工作。 从 1.3.0 版本开始,WhateverGreen 提供了对 LSPCON 的驱动支持。驱动会自动将转换器调为 PCON 模式以解决 HDMI 2.0 输出黑屏问题。 @@ -1764,15 +1764,16 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest #### 如何使用 - 为核显添加 `enable-lspcon-support` 属性或者直接使用 `-igfxlspcon` 启动参数来启用驱动。 -- 接下来你需要知道 HDMI 2.0 对应的端口号是多少。这个你可以直接在 IORegistryExplorer 里看到。在 AppleIntelFramebuffer@0/1/2/3 下面找到你的外接显示器。 -*如果你身边只有 2K/4K HDMI 显示器的话,你需要先启用上面的死循环修复补丁,否则当你连接显示器时,系统直接死机,所以就看不到对应的端口号了。* -- 为核显添加 `framebuffer-conX-has-lspcon` 属性来通知驱动哪个接口下面有 LSPCON 信号转换器。把 `conX` 里的 X 替换成你在上一步找到的端口值。 +- 接下来你需要知道 HDMI 2.0 对应的端口号是多少。这个你可以直接在 IORegistryExplorer 里看到。也就是在 `AppleIntelFramebuffer@0/1/2/3` 下面找到你的外接显示器。 +*如果你身边只有 2K/4K HDMI 显示器的话,你可能需要先启用上面的死循环修复补丁,否则当你连接显示器时系统直接死机,所以就看不到对应的端口号了。* +- 为核显添加 `framebuffer-conX-has-lspcon` 属性来通知驱动哪个接口下面有 LSPCON 信号转换器。 +把 `conX` 里的 X 替换成你在上一步找到的端口值。 这个属性的对应值请设为 `Data` 类型。如果接口下存在转换器的话,请设为 `01000000`,反之设为 `00000000`。 若不定义这个属性的话,驱动默认认为对应接口下**不存在**转换器。 -- (*可选*) 为核显添加 `framebuffer-conX-preferred-lspcon-mode` 属性以指定 LSPCON 信号转换器应该工作在何种模式下。 +- *(可选)* 为核显添加 `framebuffer-conX-preferred-lspcon-mode` 属性以指定 LSPCON 应该工作在何种模式下。 这个属性的对应值请设为 `Data` 类型。 如果希望转换器工作在 PCON (DP 转 HDMI 2.0) 模式下的话,请设为 `01000000`。 -如果希望转换器工作在 LS (DP 转 HDMI 1.4) 模式下的话,请设为 `00000000`。 +如果希望转换器工作在   LS (DP 转 HDMI 1.4) 模式下的话,请设为 `00000000`。 若指定其他值的话,驱动默认认为转换器应工作在 PCON 模式下。 若不定义此属性的话,同上。 ![](Img/lspcon.png) @@ -1814,9 +1815,11 @@ igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. ``` -另外你也能在 IORegistryExplorer 下找到驱动在对应的 Framebuffer 下注入的属性。(此功能仅限 DEBUG 版驱动) +另外你也能在 IORegistryExplorer 下找到驱动在对应的 Framebuffer 下注入的属性。**(此功能仅限 DEBUG 版驱动)** + `fw-framebuffer-has-lspcon` 显示当前端口是否存在 LSPCON 信号转换器,为布尔值类型。 `fw-framebuffer-preferred-lspcon-mode` 显示当前指定的 LSPCON 工作模式,为数据类型。1 为 PCON 模式,0 为 LS 模式。 + ![](Img/lspcon_debug.png) diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index f49ad94a..a417dcb9 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1669,41 +1669,44 @@ If this property is not specified, same as above. Add the `-igfxhdmidivs` boot argument to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. #### General Notes - For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their HDMI 1.4 port, you might find this fix helpful. -- For those who have a laptop or PC with HDMI 2.0 routed to Intel IGPU and have HDMI output issues, please note that this fix is now succeeded by the LSPCON driver solution, and it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. +- For those who have a laptop or PC with HDMI 2.0 routed to Intel IGPU and have HDMI output issues, please note that this fix is now succeeded by the LSPCON driver solution, and it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. *(You might still need this fix temporarily to figure out the framebuffer index of your HDMI port. See the LSPCON section below.)* ## LSPCON driver support to enable DisplayPort to HDMI 2.0 output on Intel IGPU #### Brief Introduction Recent laptops (Kaby Lake/Coffee Lake-based) are typically equipped with a HDMI 2.0 port. This port could be either routed to IGPU or DGPU, and you can have a confirmation on Windows 10. Intel (U)HD Graphics, however, does not provide native HDMI 2.0 output, so in order to solve this issue OEMs add an additional hardware named LSPCON on the motherboard to convert DisplayPort into HDMI 2.0. + LSPCON works in either Level Shifter (LS) or Protocol Converter (PCON) mode. When the adapter works in LS mode, it is capable of producing HDMI 1.4 signals from DisplayPort, while in PCON mode, it could provide HDMI 2.0 output. Some onboard LSPCON adapters (e.g. the one on Dell XPS 15 9570) have been configured in the firmware to work in LS mode by default, resulting a black screen on handling HDMI 2.0 connections. + Starting from version 1.3.0, WhateverGreen now provides driver support for the onboard LSPCON by automatically configuring the adapter to run in PCON mode on new HDMI connections, and hence solves the black screen issue on some platforms. #### Before you start - LSPCON driver is only applicable for laptops and PCs **with HDMI 2.0 routed to Intel IGPU**. - LSPCON driver is necessary for all newer platforms unless the new Intel IGPU starts to provide native HDMI 2.0 output. - Supported Intel Platform: Skylake, Kaby Lake, Coffee Lake and later. -Skylake: Intel NUC Skull Canyon; Iris Pro 580 + HDMI 2.0 with Parade PS175 LSPCON. -Coffee Lake: Some laptops, Dell XPS 15 9570 for example, are equipped with HDMI 2.0 and Parade PS175 LSPCON. +Skylake Case: Intel NUC Skull Canyon; Iris Pro 580 + HDMI 2.0 with Parade PS175 LSPCON. +Coffee Lake Case: Some laptops, e.g. Dell XPS 15 9570, are equipped with HDMI 2.0 and Parade PS175 LSPCON. - If you have confirmed that your HDMI 2.0 is routed to Intel IGPU and is working properly right now, you don't need to enable this driver, because your onboard LSPCON might already be configured in the firmware to work in PCON mode. #### Instructions - Add the `enable-lspcon-support` property to `IGPU` to enable the driver, or use the boot-arg `-igfxlspcon` instead. -- Next, you need to know the corresponding connector index (one of 0,1,2,3) of your HDMI port. You could find it under IGPU in IORegistryExplorer. -*If you only have a 2K/4K HDMI monitor, you might need to enable the infinite loop fix before connecting a HDMI monitor to your build, otherwise the system just hangs and you won't be able to run the IORegistryExplorer and find the framebuffer index.* +- Next, you need to know the corresponding connector index (one of 0,1,2,3) of your HDMI port. You could find it under IGPU in IORegistryExplorer. (i.e. `AppleIntelFramebuffer@0/1/2/3`) +*If you only have a 2K/4K HDMI monitor, you might need to enable the infinite loop fix before connecting a HDMI monitor to your build, otherwise the system just hangs, so you won't be able to run the IORegistryExplorer and find the framebuffer index.* - Add the `framebuffer-conX-has-lspcon` property to `IGPU` to inform the driver which connector has an onboard LSPCON adapter. Replace `X` with the index you have found in the previous step. -The value must be of type `Data` and must be one of `01000000` (True) and `00000000` (False). +The value must be of type `Data` and should be one of `01000000` (True) and `00000000` (False). - (*Optional*) Add the `framebuffer-conX-preferred-lspcon-mode` property to `IGPU` to specify a mode for your onboard LSPCON adapter. -The value must be of type `Data` and must be one of `01000000` (PCON, DP to HDMI 2.0) and `00000000` (LS, DP to HDMI 1.4). +The value must be of type `Data` and should be one of `01000000` (PCON, DP to HDMI 2.0) and `00000000` (LS, DP to HDMI 1.4). Any other invalid values are treated as PCON mode. If this property is not specified, the driver assumes that PCON mode is preferred. ![](Img/lspcon.png) #### Debugging Once you have completed the steps above, rebuild the kext cache and reboot your computer. -After plugging into your HDMI 2.0 cable (and the HDMI 2.0 monitor), you should be able to see the output on your monitor. -Dump your kernel log, and you should also be able to see something simillar to lines below. +After plugging into your HDMI 2.0 cable (and the HDMI 2.0 monitor), you should be able to see the output on your monitor. + +Dump your kernel log and you should also be able to see something simillar to lines below. ``` -// When you plug the HDMI 2.0 cable +// When you insert the HDMI 2.0 cable igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] called with controller at 0xffffff81a8680000 and framebuffer at 0xffffff81a868c000. igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] No LSPCON chip associated with this framebuffer. igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB0] Will call the original method. @@ -1735,9 +1738,12 @@ igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Will call the original method. igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. ``` -Additionally, you can find these properties injected by the driver under the corresponding framebuffer. (Only available in DEBUG version) -`fw-framebuffer-has-lspcon` indicates whether the onboard LSPCON adapter exists or not. -`fw-framebuffer-preferred-lspcon-mode` indicates the preferred adapter mode. 1 is PCON, and 0 is LS. +Additionally, you can find these properties injected by the driver under the corresponding framebuffer. +**(Only available in DEBUG version)** + +`fw-framebuffer-has-lspcon` indicates whether the onboard LSPCON adapter exists or not. +`fw-framebuffer-preferred-lspcon-mode` indicates the preferred adapter mode. 1 is PCON, and 0 is LS. + ![](Img/lspcon_debug.png) ## Known Issues From 079bea85d506ffe180f927b97e00f08dc6c36ea7 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 22 Jun 2019 23:51:16 +0800 Subject: [PATCH 018/102] Attempt to fix the memory analyzer warning. --- WhateverGreen/kern_igfx.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 691b9f4d..c2f160f3 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1051,9 +1051,9 @@ IOReturn IGFX::LSPCON::wakeUpNativeAUX() { IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { if (callbackIGFX->verboseI2C) { - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); DBGLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", - index->unsigned32BitValue(), address, length, intermediate, flags); + index, address, length, intermediate, flags); IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); DBGLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); return retVal; @@ -1064,9 +1064,9 @@ IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { if (callbackIGFX->verboseI2C) { - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); DBGLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", - index->unsigned32BitValue(), address, length, intermediate); + index, address, length, intermediate); IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); DBGLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); return retVal; From 040722805d3bb6cc2fecd2dc0436794b012f99d6 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Jun 2019 00:02:19 +0800 Subject: [PATCH 019/102] Improve the verbose output when -igfxi2cverbose is specified. --- WhateverGreen/kern_igfx.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index c2f160f3..c7a7b84b 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1052,10 +1052,10 @@ IOReturn IGFX::LSPCON::wakeUpNativeAUX() { IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { if (callbackIGFX->verboseI2C) { auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); - DBGLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", + SYSLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", index, address, length, intermediate, flags); IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); - DBGLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); + SYSLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); return retVal; } else { return callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); @@ -1065,10 +1065,10 @@ IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { if (callbackIGFX->verboseI2C) { auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); - DBGLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", + SYSLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", index, address, length, intermediate); IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); - DBGLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); + SYSLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); return retVal; } else { return callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); From a444a34df2d9826f84045d5dd963ebe37a78d6ad Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Jun 2019 00:08:45 +0800 Subject: [PATCH 020/102] Fix the unused variable warning under RELEASE configurations. --- WhateverGreen/kern_igfx.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index c7a7b84b..83b67d10 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -942,11 +942,10 @@ IOReturn IGFX::LSPCON::probe() { } // Parse the chip vendor - Vendor vendor = parseVendor(info); char device[8]; bzero(device, 8); lilu_os_memcpy(device, info->deviceID, 6); - DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, getVendorString(vendor), device); + DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, getVendorString(parseVendor(info)), device); // Parse the current adapter mode Mode mode = parseMode(info->lspconCurrentMode); From 7222f52fbc936b4db79b6350f86f9de3d5fbde6f Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Jun 2019 21:45:36 +0800 Subject: [PATCH 021/102] Improve the error message in ComputeHdmiP0P1P2. --- WhateverGreen/kern_igfx.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 83b67d10..7fcd0f0a 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -900,9 +900,10 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa uint32_t fraction = (uint32_t) (context.frequency - multiplier * 24000000); uint32_t cf15625 = (uint32_t) (context.central / 15625); DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Multiplier = %d; Fraction = %d; CF15625 = %d.\n", multiplier, fraction, cf15625); + // Guard: The given CRTC parameters should never be NULL auto params = reinterpret_cast(parameters); if (params == nullptr) { - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: Failed to cast the given CRTC parameter pointer."); + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: The given CRTC parameters should not be NULL."); return 0; } params->pdiv = context.pdiv; From b97c13addf8fa353bb46b86e603cf4bda6d884a5 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 24 Jun 2019 23:23:45 +0800 Subject: [PATCH 022/102] Improve the code style; several minor fixes. --- Changelog.md | 0 Manual/FAQ.IntelHD.cn.md | 2 +- Manual/FAQ.IntelHD.en.md | 2 +- Manual/Img/lspcon.png | Bin Manual/Img/lspcon_debug.png | Bin README.md | 2 +- WhateverGreen/kern_igfx.cpp | 237 +++++++++--------- WhateverGreen/kern_igfx.hpp | 477 +++++++++++++++++++----------------- 8 files changed, 374 insertions(+), 346 deletions(-) mode change 100644 => 100755 Changelog.md mode change 100644 => 100755 Manual/FAQ.IntelHD.cn.md mode change 100644 => 100755 Manual/FAQ.IntelHD.en.md mode change 100644 => 100755 Manual/Img/lspcon.png mode change 100644 => 100755 Manual/Img/lspcon_debug.png mode change 100644 => 100755 README.md mode change 100644 => 100755 WhateverGreen/kern_igfx.cpp mode change 100644 => 100755 WhateverGreen/kern_igfx.hpp diff --git a/Changelog.md b/Changelog.md old mode 100644 new mode 100755 diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md old mode 100644 new mode 100755 index d18b3c7b..b523ce82 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1737,7 +1737,7 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复核显驱动在尝试点亮外接 HDMI 高分辨率显示器时造成的死循环问题 **适用平台:** 第六代酷睿 Skylake 核显,第七代酷睿 Kaby Lake 核显以及第八代酷睿 Coffee Lake。 -使用 `-igfxhdmidivs` 启动参数以解决核显驱动在试图点亮外接 HDMI 高分辨率显示器时造成的系统死机问题。 +为核显添加 `enable-hdmi-dividers-fix` 属性或者直接使用 `-igfxhdmidivs` 启动参数以解决核显驱动在试图点亮外接 HDMI 高分辨率显示器时造成的系统死机问题。 具体症状表现为插入 HDMI 线后,笔记本内屏变黑但有背光,系统无响应,并且外屏也无输出。 #### 关于使用此修复补丁的一些建议 - 如果你的笔记本或台式机主板有 HDMI 1.4 接口,并且想使用 2K 或 4K HDMI 显示器的话,你可能需要这个补丁。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md old mode 100644 new mode 100755 index a417dcb9..9bda5c2c --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1666,7 +1666,7 @@ If an invalid value is specified, the default value `0x14` will be used instead. If this property is not specified, same as above. ## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on Skylake, Kaby Lake and Coffee Lake platforms -Add the `-igfxhdmidivs` boot argument to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. +Add the `enable-hdmi-dividers-fix` property to `IGPU` or use the `-igfxhdmidivs` boot argument instead to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. #### General Notes - For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their HDMI 1.4 port, you might find this fix helpful. - For those who have a laptop or PC with HDMI 2.0 routed to Intel IGPU and have HDMI output issues, please note that this fix is now succeeded by the LSPCON driver solution, and it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. diff --git a/Manual/Img/lspcon.png b/Manual/Img/lspcon.png old mode 100644 new mode 100755 diff --git a/Manual/Img/lspcon_debug.png b/Manual/Img/lspcon_debug.png old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index f56478f7..102d5e58 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Read [FAQs](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/) an - `igfxcflbklt=1` boot argument (and `enable-cfl-backlight-fix` property) to enable CFL backlight patch - `applbkl=0` boot argument to disable AppleBacklight.kext patches for IGPU. In case of custom AppleBacklight profile- [read here.](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.OldPlugins.en.md) - `-igfxmlr` boot argument (and `enable-dpcd-max-link-rate-fix` property) to apply the maximum link rate fix. -- `-igfxhdmidivs` boot argument to fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on SKL, KBL and CFL platforms. +- `-igfxhdmidivs` boot argument (and `enable-hdmi-dividers-fix` property) to fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on SKL, KBL and CFL platforms. - `-igfxlspcon` boot argument (and `enable-lspcon-support` property) to enable the driver support for onboard LSPCON chips. [Read the manual](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.IntelHD.en.md) - `-igfxi2cdbg` boot argument to enable verbose output in I2C-over-AUX transactions (only for debugging purposes). diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp old mode 100644 new mode 100755 index 7fcd0f0a..f6633b61 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -141,11 +141,9 @@ void IGFX::init() { } void IGFX::deinit() { - for (int index = 0; index < arrsize(lspcons); index++) { - if (lspcons[index].lspcon != nullptr) { - delete lspcons[index].lspcon; - lspcons[index].lspcon = nullptr; - } + for (auto &con : lspcons) { + LSPCON::deleter(con.lspcon); + con.lspcon = nullptr; } } @@ -167,6 +165,9 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { // Enable the fix for computing HDMI dividers on SKL, KBL, CFL platforms if the corresponding boot argument is found hdmiP0P1P2Patch = checkKernelArgument("-igfxhdmidivs"); + // Of if "enable-hdmi-dividers-fix" is set in IGPU property + if (!hdmiP0P1P2Patch) + hdmiP0P1P2Patch = info->videoBuiltin->getProperty("enable-hdmi-dividers-fix") != nullptr; // Enable the LSPCON driver support if the corresponding boot argument is found supportLSPCON = checkKernelArgument("-igfxlspcon"); @@ -179,7 +180,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { char name[48]; uint32_t pmode = 0x01; // PCON mode as a fallback value for (size_t index = 0; index < arrsize(lspcons); index++) { - bzero(name, 48); + bzero(name, sizeof(name)); snprintf(name, sizeof(name), "framebuffer-con%lu-has-lspcon", index); WIOKit::getOSDataValue(info->videoBuiltin, name, lspcons[index].hasLSPCON); snprintf(name, sizeof(name), "framebuffer-con%lu-preferred-lspcon-mode", index); @@ -411,13 +412,11 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (hdmiP0P1P2Patch) { auto ppp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController17ComputeHdmiP0P1P2EjP21AppleIntelDisplayPathPNS_10CRTCParamsE", address, size); if (ppp) { - patcher.eraseCoverageInstPrefix(ppp); - orgComputeHdmiP0P1P2 = reinterpret_cast(patcher.routeFunction(ppp, reinterpret_cast(wrapComputeHdmiP0P1P2), true)); - if (orgComputeHdmiP0P1P2) { - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() has been routed successfully."); - } else { + if (patcher.routeFunction(ppp, reinterpret_cast(wrapComputeHdmiP0P1P2))) { patcher.clearError(); SYSLOG("igfx", "SC: Failed to route ComputeHdmiP0P1P2()."); + } else { + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() has been routed successfully."); } } else { SYSLOG("igfx", "SC: Failed to find ComputeHdmiP0P1P2()."); @@ -729,7 +728,7 @@ void IGFX::populateP0P1P2(struct ProbeContext *context) { // Even divider if (p % 2 == 0) { - unsigned int half = p / 2; + uint32_t half = p / 2; if (half == 1 || half == 2 || half == 3 || half == 5) { p0 = 2; p1 = 1; @@ -813,6 +812,8 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa // - 2019.06 // + static_assert(__offsetof(CRTCParams, pdiv) == 0x20, "Invalid pdiv offset, please check your compiler."); + DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Called with pixel clock = %d Hz.", pixelClock); /// All possible dividers @@ -834,8 +835,7 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa uint64_t afeClock = pixelClock * 5; // Prepare the context for probing P0, P1 and P2 - struct ProbeContext context; - bzero(&context, sizeof(struct ProbeContext)); + ProbeContext context {}; // Apple chooses 400 as the initial minimum deviation // However 400 is too small for a pixel clock like 533.25 MHz (HDMI 2.0 4K @ 60Hz) @@ -843,10 +843,8 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa // It's OK because the deviation is still bound by MAX_POS_DEV and MAX_NEG_DEV. context.minDeviation = UINT64_MAX; - for (int dindex = 0; dindex < sizeof(dividers) / sizeof(int); dindex++) { - for (int cfindex = 0; cfindex < sizeof(centralFrequencies) / sizeof(uint64_t); cfindex++) { - int divider = dividers[dindex]; - uint64_t central = centralFrequencies[cfindex]; + for (auto divider : dividers) { + for (auto central : centralFrequencies) { // Calculate the current DCO frequency uint64_t frequency = divider * afeClock; // Calculate the deviation @@ -901,11 +899,11 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa uint32_t cf15625 = (uint32_t) (context.central / 15625); DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Multiplier = %d; Fraction = %d; CF15625 = %d.\n", multiplier, fraction, cf15625); // Guard: The given CRTC parameters should never be NULL - auto params = reinterpret_cast(parameters); - if (params == nullptr) { + if (parameters == nullptr) { DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: The given CRTC parameters should not be NULL."); return 0; } + auto params = reinterpret_cast(parameters); params->pdiv = context.pdiv; params->qdiv = context.qdiv; params->kdiv = context.kdiv; @@ -916,18 +914,9 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa return 0; } -IGFX::LSPCON::LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath) { - this->controller = controller; - this->framebuffer = framebuffer; - this->displayPath = displayPath; - this->isActive = false; - this->index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); -} - IOReturn IGFX::LSPCON::probe() { // Read the adapter info - uint8_t buffer[128]; - bzero(buffer, 128); + uint8_t buffer[128] {}; IOReturn retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, 0x00, 128, buffer, 0); if (retVal != kIOReturnSuccess) { SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Failed to read the LSPCON adapter info. RV = 0x%llx.", index, retVal); @@ -943,49 +932,40 @@ IOReturn IGFX::LSPCON::probe() { } // Parse the chip vendor - char device[8]; - bzero(device, 8); + char device[8] {}; lilu_os_memcpy(device, info->deviceID, 6); DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, getVendorString(parseVendor(info)), device); // Parse the current adapter mode Mode mode = parseMode(info->lspconCurrentMode); DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] The current adapter mode is %s.", index, getModeString(mode)); - switch (mode) { - case Mode::LevelShifter: - break; - - case Mode::ProtocolConverter: - isActive = true; - break; - - default: - SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); - break; - } - + if (mode == Mode::ProtocolConverter) + isActive = true; + else if (mode != Mode::LevelShifter) + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); return kIOReturnSuccess; } IOReturn IGFX::LSPCON::getMode(Mode *mode) { - uint8_t byte; IOReturn retVal = kIOReturnAborted; // Guard: The given `mode` pointer cannot be NULL if (mode != nullptr) { - // Try at most 5 times + // Try to read the current mode from the adapter at most 5 times for (int attempt = 0; attempt < 5; attempt++) { // Read from the adapter @ 0x40; offset = 0x41 - retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CURRENT_MODE, 1, &byte, 0); + uint8_t hwModeValue; + retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CURRENT_MODE, 1, &hwModeValue, 0); // Guard: Can read the current adapter mode successfully if (retVal == kIOReturnSuccess) { - DBGLOG("igfx", "SC: LSPCON::getMode() DInfo: [FB%d] The current mode value is 0x%02x.", index, byte); - *mode = parseMode(byte); + DBGLOG("igfx", "SC: LSPCON::getMode() DInfo: [FB%d] The current mode value is 0x%02x.", index, hwModeValue); + *mode = parseMode(hwModeValue); break; } - // Sleep 1 ms + // Sleep 1 ms just in case the adapter + // is busy processing other I2C requests IOSleep(1); } } @@ -999,8 +979,8 @@ IOReturn IGFX::LSPCON::setMode(Mode newMode) { return kIOReturnAborted; // Guard: Write the new mode - uint8_t byte = getModeValue(newMode); - IOReturn retVal = callbackIGFX->advWriteI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CHANGE_MODE, 1, &byte, 0); + uint8_t hwModeValue = getModeValue(newMode); + IOReturn retVal = callbackIGFX->advWriteI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CHANGE_MODE, 1, &hwModeValue, 0); if (retVal != kIOReturnSuccess) { SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to set the new adapter mode. RV = 0x%llx.", index, retVal); return retVal; @@ -1051,7 +1031,9 @@ IOReturn IGFX::LSPCON::wakeUpNativeAUX() { IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { if (callbackIGFX->verboseI2C) { - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + auto index = idxnum != nullptr ? idxnum->unsigned32BitValue() : -1; + //->unsigned32BitValue(); SYSLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", index, address, length, intermediate, flags); IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); @@ -1064,7 +1046,8 @@ IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { if (callbackIGFX->verboseI2C) { - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + auto index = idxnum != nullptr ? idxnum->unsigned32BitValue() : -1; SYSLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", index, address, length, intermediate); IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); @@ -1186,6 +1169,76 @@ IOReturn IGFX::advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); } +void IGFX::framebufferSetupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath) { + // Retrieve the framebuffer index + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + if (idxnum == nullptr) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: Failed to retrieve the framebuffer index."); + return; + } + auto index = idxnum->unsigned32BitValue(); + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); + + // Retrieve the user preference + LSPCON *lspcon = nullptr; + auto pmode = framebufferLSPCONGetPreferredMode(index); +#ifdef DEBUG + framebuffer->setProperty("fw-framebuffer-has-lspcon", framebufferHasLSPCON(index)); + framebuffer->setProperty("fw-framebuffer-preferred-lspcon-mode", LSPCON::getModeValue(pmode), 8); +#endif + + // Guard: Check whether this framebuffer connector has an onboard LSPCON chip + if (!framebufferHasLSPCON(index)) { + // No LSPCON chip associated with this connector + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] No LSPCON chip associated with this framebuffer.", index); + return; + } + + // Guard: Check whether the LSPCON driver has already been initialized for this framebuffer + if (framebufferHasLSPCONInitialized(index)) { + // Already initialized + lspcon = framebufferGetLSPCON(index); + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver (at 0x%llx) has already been initialized for this framebuffer.", index, lspcon); + // Confirm that the adapter is running in preferred mode + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to update the mode.", index); + } + // Wake up the native AUX channel if PCON mode is preferred + if (pmode == LSPCON::Mode::ProtocolConverter && lspcon->wakeUpNativeAUX() != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to wake up the native AUX channel.", index); + } + return; + } + + // User has specified the existence of onboard LSPCON + // Guard: Initialize the driver for this framebuffer + lspcon = LSPCON::create(that, framebuffer, displayPath); + if (lspcon == nullptr) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to initialize the LSPCON driver.", index); + return; + } + + // Guard: Attempt to probe the onboard LSPCON chip + if (lspcon->probe() != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to probe the LSPCON adapter.", index); + LSPCON::deleter(lspcon); + lspcon = nullptr; + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Abort the LSPCON driver initialization.", index); + return; + } + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has detected the onboard chip successfully.", index); + + // LSPCON driver has been initialized successfully + framebufferSetLSPCON(index, lspcon); + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has been initialized successfully.", index); + + // Guard: Set the preferred adapter mode if necessary + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to set the %s mode.", index, LSPCON::getModeString(pmode)); + } + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] The adapter is now running in preferred mode [%s].", index, LSPCON::getModeString(pmode)); +} + IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath) { // // Abstract @@ -1228,75 +1281,15 @@ IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *d // - 2019.06 // - // Retrieve the framebuffer index - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex"))->unsigned32BitValue(); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); - - // Retrieve the user preference - LSPCON *lspcon = nullptr; - auto pmode = framebufferLSPCONGetPreferredMode(index); - - // Guard: Check whether this framebuffer connector has an onboard LSPCON chip - if (!framebufferHasLSPCON(index)) { - // No LSPCON chip associated with this connector - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] No LSPCON chip associated with this framebuffer.", index); - goto org; - } - - // Guard: Check whether the LSPCON driver has already been initialized for this framebuffer - if (framebufferHasLSPCONInitialized(index)) { - // Already initialized - lspcon = framebufferGetLSPCON(index); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver (at 0x%llx) has already been initialized for this framebuffer.", index, lspcon); - // Confirm that the adapter is running in preferred mode - if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] The adapter is not running in preferred mode. Failed to update the mode.", index); - } - // Wake up the native AUX channel if PCON mode is preferred - if (pmode == LSPCON::Mode::ProtocolConverter) { - if (lspcon->wakeUpNativeAUX() != kIOReturnSuccess) - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to wake up the native AUX channel.", index); - } - goto org; - } - - // User has specified the existence of onboard LSPCON - // Guard: Initialize the driver for this framebuffer - lspcon = new LSPCON(that, framebuffer, displayPath); - if (lspcon == nullptr) { - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to initialize the LSPCON driver. Insufficient memory.", index); - goto org; - } - - // Guard: Attempt to probe the onboard LSPCON chip - if (lspcon->probe() != kIOReturnSuccess) { - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Failed to probe the LSPCON adapter.", index); - delete lspcon; - lspcon = nullptr; - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] Abort the LSPCON driver initialization.", index); - goto org; - } - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver has detected the onboard chip successfully.", index); - - // LSPCON driver has been initialized successfully - framebufferSetLSPCON(index, lspcon); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] LSPCON driver has been initialized successfully.", index); - - // Guard: Set the preferred adapter mode if necessary - if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { - SYSLOG("igfx", "SC: GetDPCDInfo() Error: [FB%d] The adapter is not running in preferred mode. Failed to set the %s mode.", index, LSPCON::getModeString(pmode)); - } - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] The adapter is running in preferred mode [%s].", index, LSPCON::getModeString(pmode)); - -#ifdef DEBUG - framebuffer->setProperty("fw-framebuffer-has-lspcon", framebufferHasLSPCON(index)); - framebuffer->setProperty("fw-framebuffer-preferred-lspcon-mode", LSPCON::getModeValue(pmode), 8); -#endif + // Configure the LSPCON adapter for the given framebuffer + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Start to configure the LSPCON adapter."); + framebufferSetupLSPCON(that, framebuffer, displayPath); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Finished configuring the LSPCON adapter."); -org: - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] Will call the original method.", index); + // Call the original method + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Will call the original method."); IOReturn retVal = callbackIGFX->orgGetDPCDInfo(that, framebuffer, displayPath); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: [FB%d] Returns 0x%llx.", index, retVal); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Returns 0x%llx.", retVal); return retVal; } diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp old mode 100644 new mode 100755 index b01bfdc6..23e1f0ba --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -253,24 +253,19 @@ class IGFX { IOReturn (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; /** - * Original AppleIntelFramebufferController::ReadI2COverAUX function + * Original AppleIntelFramebufferController::ReadI2COverAUX function */ IOReturn (*orgReadI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool, uint8_t) {nullptr}; /** - * Original AppleIntelFramebufferController::WriteI2COverAUX function + * Original AppleIntelFramebufferController::WriteI2COverAUX function */ IOReturn (*orgWriteI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool) {nullptr}; /** - * Original AppleIntelFramebufferController::GetDPCDInfo function + * Original AppleIntelFramebufferController::GetDPCDInfo function */ IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *); - - /** - * Original AppleIntelFramebufferController::ComputeHdmiP0P1P2 function - */ - int (*orgComputeHdmiP0P1P2)(void *, uint32_t, void *, void *); /** * Detected CPU generation of the host system @@ -294,7 +289,7 @@ class IGFX { /** * Set to On if Coffee Lake backlight patch type required * - boot-arg igfxcflbklt=0/1 forcibly turns patch on or off (override) - * - IGPU property enable-cfl-backlight-fix turns patch on + * - IGPU property enable-cfl-backlight-fix turns patch on * - laptop with CFL CPU and CFL IGPU drivers turns patch on */ CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; @@ -305,17 +300,17 @@ class IGFX { bool maxLinkRatePatch {false}; /** - * Set to true to enable LSPCON driver support + * Set to true to enable LSPCON driver support */ bool supportLSPCON {false}; /** - * Set to true to enable verbose output in I2C-over-AUX transactions + * Set to true to enable verbose output in I2C-over-AUX transactions */ bool verboseI2C {false}; /** - * Set to true to fix the infinite loop issue when computing dividers for HDMI connections + * Set to true to fix the infinite loop issue when computing dividers for HDMI connections */ bool hdmiP0P1P2Patch {false}; @@ -450,8 +445,7 @@ class IGFX { * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h */ - struct DPCDCap16 // 16 bytes - { + struct DPCDCap16 { // 16 bytes // DPCD Revision (DP Config Version) // Value: 0x10, 0x11, 0x12, 0x13, 0x14 uint8_t revision; @@ -495,16 +489,15 @@ class IGFX { static IOReturn wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); /** - * Reflect the `AppleIntelFramebufferController::CRTCParams` struct + * Reflect the `AppleIntelFramebufferController::CRTCParams` struct * - * @note Unlike the Intel Linux Graphics Driver, - * - Apple does not transform the `pdiv`, `qdiv` and `kdiv` fields. - * - Apple records the final central frequency divided by 15625. - * @ref static void skl_wrpll_params_populate(params:afe_clock:central_freq:p0:p1:p2:) - * @seealso https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1171 - */ - struct CRTCParams - { + * @note Unlike the Intel Linux Graphics Driver, + * - Apple does not transform the `pdiv`, `qdiv` and `kdiv` fields. + * - Apple records the final central frequency divided by 15625. + * @ref static void skl_wrpll_params_populate(params:afe_clock:central_freq:p0:p1:p2:) + * @seealso https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1171 + */ + struct CRTCParams { /// Uninvestigated fields uint8_t uninvestigated[32]; @@ -526,15 +519,13 @@ class IGFX { /// Central Frequency / 15625 [`CRTCParams` field offset 0x34] uint32_t cf15625; - /// Other fields that are not of interest - uint8_t others[0]; + /// The rest fields are not of interest }; /** - * Represents the current context of probing dividers for HDMI connections + * Represents the current context of probing dividers for HDMI connections */ - struct ProbeContext - { + struct ProbeContext { /// The current minimum deviation uint64_t minDeviation; @@ -558,59 +549,58 @@ class IGFX { }; /** - * The maximum positive deviation from the DCO central frequency + * The maximum positive deviation from the DCO central frequency * - * @note DCO frequency must be within +1% of the DCO central frequency. - * @warning This is a hardware requirement. - * See "Intel Graphics Programmer Reference Manual for Kaby Lake platform" - * Volume 12 Display, Page 134, Formula for HDMI and DVI DPLL Programming - * @link https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf - * @note This value is appropriate for graphics on Skylake, Kaby Lake and Coffee Lake platforms. - * @seealso Intel Linux Graphics Driver - * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1080 + * @note DCO frequency must be within +1% of the DCO central frequency. + * @warning This is a hardware requirement. + * See "Intel Graphics Programmer Reference Manual for Kaby Lake platform" + * Volume 12 Display, Page 134, Formula for HDMI and DVI DPLL Programming + * @link https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf + * @note This value is appropriate for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1080 */ static constexpr uint64_t SKL_DCO_MAX_POS_DEVIATION = 100; /** - * The maximum negative deviation from the DCO central frequency + * The maximum negative deviation from the DCO central frequency * - * @note DCO frequency must be within -6% of the DCO central frequency. - * @seealso See `SKL_DCO_MAX_POS_DEVIATION` above for details. + * @note DCO frequency must be within -6% of the DCO central frequency. + * @seealso See `SKL_DCO_MAX_POS_DEVIATION` above for details. */ static constexpr uint64_t SKL_DCO_MAX_NEG_DEVIATION = 600; /** - * [Helper] Compute the final P0, P1, P2 values based on the current frequency divider + * [Helper] Compute the final P0, P1, P2 values based on the current frequency divider * - * @param context The current context for probing P0, P1 and P2. - * @note Implementation adopted from the Intel Graphics Programmer Reference Manual; - * Volume 12 Display, Page 135, Algorithm to Find HDMI and DVI DPLL Programming. - * Volume 12 Display, Page 135, Pseudo-code for HDMI and DVI DPLL Programming. - * @ref static void skl_wrpll_get_multipliers(p:p0:p1:p2:) - * @seealso Intel Linux Graphics Driver - * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 + * @param context The current context for probing P0, P1 and P2. + * @note Implementation adopted from the Intel Graphics Programmer Reference Manual; + * Volume 12 Display, Page 135, Algorithm to Find HDMI and DVI DPLL Programming. + * Volume 12 Display, Page 135, Pseudo-code for HDMI and DVI DPLL Programming. + * @ref static void skl_wrpll_get_multipliers(p:p0:p1:p2:) + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 */ static void populateP0P1P2(struct ProbeContext* context); /** - * Compute dividers for a HDMI connection with the given pixel clock + * Compute dividers for a HDMI connection with the given pixel clock * - * @param that The hidden implicit `this` pointer - * @param pixelClock The pixel clock value (in Hz) used for the HDMI connection - * @param displayPath The corresponding display path - * @param parameters CRTC parameters populated on return - * @return Never used by its caller, so this method might return void. - * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` + * @param that The hidden implicit `this` pointer + * @param pixelClock The pixel clock value (in Hz) used for the HDMI connection + * @param displayPath The corresponding display path + * @param parameters CRTC parameters populated on return + * @return Never used by its caller, so this method might return void. + * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` */ static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); /** - * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 + * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 * - * Specific to LSPCON DisplayPort 1.2 to HDMI 2.0 Adapter + * Specific to LSPCON DisplayPort 1.2 to HDMI 2.0 Adapter */ - struct DisplayPortDualModeAdapterInfo // first 128 bytes - { + struct DisplayPortDualModeAdapterInfo { // first 128 bytes /// [0x00] HDMI ID /// /// Fixed Value: "DP-HDMI ADAPTOR\x04" @@ -692,13 +682,12 @@ class IGFX { }; /** - * Represents the onboard Level Shifter and Protocol Converter + * Represents the onboard Level Shifter and Protocol Converter */ - class LSPCON - { + class LSPCON { public: /** - * Represents all possible adapter modes + * Represents all possible adapter modes */ enum class Mode: uint32_t { @@ -713,10 +702,10 @@ class IGFX { }; /** - * [Mode Helper] Parse the adapter mode from the raw register value + * [Mode Helper] Parse the adapter mode from the raw register value * - * @param mode A raw register value read from the adapter. - * @return The corresponding adapter mode on success, `invalid` otherwise. + * @param mode A raw register value read from the adapter. + * @return The corresponding adapter mode on success, `invalid` otherwise. */ static inline Mode parseMode(uint8_t mode) { switch (mode & DP_DUAL_MODE_LSPCON_MODE_PCON) { @@ -732,11 +721,11 @@ class IGFX { } /** - * [Mode Helper] Get the raw value of the given adapter mode + * [Mode Helper] Get the raw value of the given adapter mode * - * @param mode A valid adapter mode - * @return The corresponding register value on success. - * If the given mode is `Invalid`, the raw value of `LS` mode will be returned. + * @param mode A valid adapter mode + * @return The corresponding register value on success. + * If the given mode is `Invalid`, the raw value of `LS` mode will be returned. */ static inline uint8_t getModeValue(Mode mode) { switch (mode) { @@ -752,7 +741,7 @@ class IGFX { } /** - * [Mode Helper] Get the string representation of the adapter mode + * [Mode Helper] Get the string representation of the adapter mode */ static inline const char *getModeString(Mode mode) { switch (mode) { @@ -768,70 +757,90 @@ class IGFX { } /** - * Initialize the LSPCON chip for the given framebuffer + * [Convenient] Create the LSPCON driver for the given framebuffer + * + * @param controller The opaque `AppleIntelFramebufferController` instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @return A driver instance on success, `nullptr` otherwise. + * @note This convenient initializer returns `nullptr` if it could not retrieve the framebuffer index. + */ + static LSPCON *create(void *controller, IORegistryEntry *framebuffer, void *displayPath) { + // Guard: Framebuffer index should exist + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + if (idxnum == nullptr) { + return nullptr; + } + // Call the private constructor + return new LSPCON(controller, framebuffer, displayPath, idxnum->unsigned32BitValue()); + } + + /** + * [Convenient] Destroy the LSPCON driver safely * - * @param controller The opaque `AppleIntelFramebufferController` instance - * @param framebuffer The framebuffer that owns this LSPCON chip - * @param displayPath The corresponding opaque display path instance + * @param instance A nullable LSPCON driver instance */ - LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath); + static void deleter(LSPCON *instance) { + if (instance != nullptr) + delete instance; + } /** - * Probe the onboard LSPCON chip + * Probe the onboard LSPCON chip * - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method will mark the adapter active if it has found that the adpater is already in `PCON` mode. - * @see `isActive` and `isAdapterActive()`. + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method will mark the adapter active if it has found that the adpater is already in `PCON` mode. + * @see `isActive` and `isAdapterActive()`. */ IOReturn probe(); /** - * Get the current adapter mode + * Get the current adapter mode * - * @param mode The current adapter mode on return. - * @return `kIOReturnSuccess` on success, errors otherwise. + * @param mode The current adapter mode on return. + * @return `kIOReturnSuccess` on success, errors otherwise. */ IOReturn getMode(Mode *mode); /** - * Change the adapter mode + * Change the adapter mode * - * @param newMode The new adapter mode - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method will not return until `newMode` is effective. - * @note This method will mark the adapter active if `newMode` is `PCON`and becomes effective. - * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method will not return until `newMode` is effective. + * @note This method will mark the adapter active if `newMode` is `PCON`and becomes effective. + * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. */ IOReturn setMode(Mode newMode); /** - * Change the adapter mode if necessary + * Change the adapter mode if necessary * - * @param newMode The new adapter mode - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method is a wrapper of `setMode` and will only set the new mode if `newMode` is not currently effective. - * @seealso `setMode(newMode:)` + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method is a wrapper of `setMode` and will only set the new mode if `newMode` is not currently effective. + * @seealso `setMode(newMode:)` */ IOReturn setModeIfNecessary(Mode newMode); /** - * Wake up the native DisplayPort AUX channel for this adapter + * Wake up the native DisplayPort AUX channel for this adapter * - * @return `kIOReturnSuccess` on success, other errors otherwise. + * @return `kIOReturnSuccess` on success, other errors otherwise. */ IOReturn wakeUpNativeAUX(); /** - * Return `true` if the adapter is active + * Return `true` if the adapter is active */ inline bool isAdapterActive() { - return this->isActive; + return isActive; } /** - * Return `true` if the adapter is running in the given mode + * Return `true` if the adapter is running in the given mode * - * @param mode The expected mode; one of `LS` and `PCON` + * @param mode The expected mode; one of `LS` and `PCON` */ inline bool isRunningInMode(Mode mode) { // isActive 1 1 0 0 @@ -869,7 +878,7 @@ class IGFX { static constexpr uint8_t DP_DUAL_MODE_TYPE_HAS_DPCD = 0x08; /** - * Represents all possible chip vendors + * Represents all possible chip vendors */ enum class Vendor { MegaChips, @@ -894,10 +903,27 @@ class IGFX { uint32_t index; /** - * [Vendor Helper] Parse the adapter vendor from the adapter info + * Initialize the LSPCON chip for the given framebuffer + * + * @param controller The opaque `AppleIntelFramebufferController` instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @param index The framebuffer index (only for debugging purposes) + * @seealso LSPCON::create(controller:framebuffer:displayPath:) to create the driver instance. + */ + LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath, uint32_t index) { + this->controller = controller; + this->framebuffer = framebuffer; + this->displayPath = displayPath; + this->isActive = false; + this->index = index; + } + + /** + * [Vendor Helper] Parse the adapter vendor from the adapter info * - * @param info A non-null DP++ adapter info instance - * @return The vendor on success, `Unknown` otherwise. + * @param info A non-null DP++ adapter info instance + * @return The vendor on success, `Unknown` otherwise. */ static inline Vendor parseVendor(DisplayPortDualModeAdapterInfo *info) { uint32_t oui = info->oui[0] << 16 | info->oui[1] << 8 | info->oui[2]; @@ -914,7 +940,7 @@ class IGFX { } /** - * [Vendor Helper] Get the string representation of the adapter vendor + * [Vendor Helper] Get the string representation of the adapter vendor */ static inline const char *getVendorString(Vendor vendor) { switch (vendor) { @@ -930,20 +956,20 @@ class IGFX { } /** - * [DP++ Helper] Check whether this is a HDMI adapter based on the adapter info + * [DP++ Helper] Check whether this is a HDMI adapter based on the adapter info * - * @param info A non-null DP++ adapter info instance - * @return `true` if this is a HDMI adapter, `false` otherwise. + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a HDMI adapter, `false` otherwise. */ static inline bool isHDMIAdapter(DisplayPortDualModeAdapterInfo *info) { return memcmp(info->hdmiID, "DP-HDMI ADAPTOR\x04", 16) == 0; } /** - * [DP++ Helper] Check whether this is a LSPCON adapter based on the adapter info + * [DP++ Helper] Check whether this is a LSPCON adapter based on the adapter info * - * @param info A non-null DP++ adapter info instance - * @return `true` if this is a LSPCON DP-HDMI adapter, `false` otherwise. + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a LSPCON DP-HDMI adapter, `false` otherwise. */ static inline bool isLSPCONAdapter(DisplayPortDualModeAdapterInfo *info) { // Guard: Check whether it is a DP to HDMI adapter @@ -956,85 +982,94 @@ class IGFX { }; /** - * Represents the LSPCON chip info for a framebuffer + * Represents the LSPCON chip info for a framebuffer */ - struct FramebufferLSPCON - { + struct FramebufferLSPCON { /** - * Indicate whether this framebuffer has an onboard LSPCON chip + * Indicate whether this framebuffer has an onboard LSPCON chip * - * @note This value will be read from the IGPU property `framebuffer-conX-has-lspcon`. - * @warning If not specified, assuming no onboard LSPCON chip for this framebuffer. + * @note This value will be read from the IGPU property `framebuffer-conX-has-lspcon`. + * @warning If not specified, assuming no onboard LSPCON chip for this framebuffer. */ uint32_t hasLSPCON {0}; /** - * User preferred LSPCON adapter mode + * User preferred LSPCON adapter mode * - * @note This value will be read from the IGPU property `framebuffer-conX-preferred-lspcon-mode`. - * @warning If not specified, assuming `PCON` mode is preferred. - * @warning If invalid mode value found, assuming `PCON` mode + * @note This value will be read from the IGPU property `framebuffer-conX-preferred-lspcon-mode`. + * @warning If not specified, assuming `PCON` mode is preferred. + * @warning If invalid mode value found, assuming `PCON` mode */ LSPCON::Mode preferredMode {LSPCON::Mode::ProtocolConverter}; /** - * The corresponding LSPCON driver; `NULL` if no onboard chip + * The corresponding LSPCON driver; `NULL` if no onboard chip */ LSPCON *lspcon {nullptr}; }; /** - * User-defined LSPCON chip info for all possible framebuffers + * User-defined LSPCON chip info for all possible framebuffers */ FramebufferLSPCON lspcons[MaxFramebufferConnectorCount]; /// MARK:- Manage user-defined LSPCON chip info for all framebuffers /** - * [Convenient] Check whether the given framebuffer has an onboard LSPCON chip + * Setup the LSPCON driver for the given framebuffer + * + * @param that The opaque framebuffer controller instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @note This method will update fields in `lspcons` accordingly on success. + */ + static void framebufferSetupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath); + + /** + * [Convenient] Check whether the given framebuffer has an onboard LSPCON chip * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. */ static inline bool framebufferHasLSPCON(uint32_t index) { return callbackIGFX->lspcons[index].hasLSPCON; } /** - * [Convenient] Check whether the given framebuffer already has LSPCON driver initialized + * [Convenient] Check whether the given framebuffer already has LSPCON driver initialized * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. */ static inline bool framebufferHasLSPCONInitialized(uint32_t index) { return callbackIGFX->lspcons[index].lspcon != nullptr; } /** - * [Convenient] Get the non-null LSPCON driver associated with the given framebuffer + * [Convenient] Get the non-null LSPCON driver associated with the given framebuffer * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return The LSPCON driver instance. + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The LSPCON driver instance. */ static inline LSPCON *framebufferGetLSPCON(uint32_t index) { return callbackIGFX->lspcons[index].lspcon; } /** - * [Convenient] Set the non-null LSPCON driver for the given framebuffer + * [Convenient] Set the non-null LSPCON driver for the given framebuffer * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer */ static inline void framebufferSetLSPCON(uint32_t index, LSPCON *lspcon) { callbackIGFX->lspcons[index].lspcon = lspcon; } /** - * [Convenient] Get the preferred LSPCON mode for the given framebuffer + * [Convenient] Get the preferred LSPCON mode for the given framebuffer * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return The preferred adapter mode. + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The preferred adapter mode. */ static inline LSPCON::Mode framebufferLSPCONGetPreferredMode(uint32_t index) { return callbackIGFX->lspcons[index].preferredMode; @@ -1043,115 +1078,115 @@ class IGFX { /// MARK:- I2C-over-AUX Transaction APIs /** - * [Advanced] Reposition the offset for an I2C-over-AUX access + * [Advanced] Reposition the offset for an I2C-over-AUX access * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset The address of the next register to access - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advSeekI2COverAUX(framebuffer:displayPath:address:offset:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset The address of the next register to access + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advSeekI2COverAUX(framebuffer:displayPath:address:offset:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. */ static IOReturn advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags); /** - * [Advanced] Read from an I2C slave via the AUX channel + * [Advanced] Read from an I2C slave via the AUX channel * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset Address of the first register to read from - * @param length The number of bytes requested to read starting from `offset` - * @param buffer A non-null buffer to store the bytes - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advReadI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to read from + * @param length The number of bytes requested to read starting from `offset` + * @param buffer A non-null buffer to store the bytes + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advReadI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. */ static IOReturn advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); /** - * [Advanced] Write to an I2C slave via the AUX channel + * [Advanced] Write to an I2C slave via the AUX channel * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset Address of the first register to write to - * @param length The number of bytes requested to write starting from `offset` - * @param buffer A non-null buffer containing the bytes to write - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advWriteI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to write to + * @param length The number of bytes requested to write starting from `offset` + * @param buffer A non-null buffer containing the bytes to write + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advWriteI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. */ static IOReturn advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); /** - * [Basic] Read from an I2C slave via the AUX channel + * [Basic] Read from an I2C slave via the AUX channel * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param length The number of bytes requested to read and must be <= 16 (See below) - * @param buffer A buffer to store the read bytes (See below) - * @param intermediate Set `true` if this is an intermediate read (See below) - * @param flags A flag reserved by Apple; currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in - * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. - * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. - * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. - * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) - * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) - * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) - * @note Method Signature: `AppleIntelFramebufferController::ReadI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:flags:)` - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::ReadI2COverAUX()` method. - * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. - * @ref TODO: Add the link to the blog post. [Working In Progress] + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to read and must be <= 16 (See below) + * @param buffer A buffer to store the read bytes (See below) + * @param intermediate Set `true` if this is an intermediate read (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::ReadI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:flags:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::ReadI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + * @ref TODO: Add the link to the blog post. [Working In Progress] */ static IOReturn wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags); /** - * [Basic] Write to an I2C adapter via the AUX channel + * [Basic] Write to an I2C adapter via the AUX channel * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param length The number of bytes requested to write and must be <= 16 (See below) - * @param buffer A buffer that stores the bytes to write (See below) - * @param intermediate Set `true` if this is an intermediate write (See below) - * @param flags A flag reserved by Apple; currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in - * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. - * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. - * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. - * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) - * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) - * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) - * @note Method Signature: `AppleIntelFramebufferController::WriteI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:)` - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::WriteI2COverAUX()` method. - * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. - * @ref TODO: Add the link to the blog post. [Working In Progress] + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to write and must be <= 16 (See below) + * @param buffer A buffer that stores the bytes to write (See below) + * @param intermediate Set `true` if this is an intermediate write (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::WriteI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::WriteI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + * @ref TODO: Add the link to the blog post. [Working In Progress] */ static IOReturn wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate); /** - * [Wrapper] Retrieve the DPCD info for a given framebuffer port + * [Wrapper] Retrieve the DPCD info for a given framebuffer port * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::GetDPCDInfo()` method. - * Used to inject code to initialize the driver for the onboard LSPCON chip. + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::GetDPCDInfo()` method. + * Used to inject code to initialize the driver for the onboard LSPCON chip. */ static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); From 7a5bd3a15bf3ffebdbdfb31b99c2cca20bd8d9fe Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 25 Jun 2019 15:05:56 +0800 Subject: [PATCH 023/102] Improve the deleter by adding the NONNULL attribute; Implement the getIndex helper. --- WhateverGreen/kern_igfx.cpp | 26 ++++++++++--------------- WhateverGreen/kern_igfx.hpp | 38 +++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index f6633b61..8075a598 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -695,17 +695,16 @@ IOReturn IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t ad // The driver tries to read the first 16 bytes from DPCD // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) // We read the value of "IOFBDependentIndex" instead of accessing that field directly - auto index = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - + uint32_t index; // Guard: Should be able to retrieve the index from the registry - if (!index) { + if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) { SYSLOG("igfx", "MLR: wrapReadAUX: Failed to read the current framebuffer index."); return retVal; } // Guard: Check the framebuffer index // By default, FB 0 refers the builtin display - if (index->unsigned32BitValue() != 0) + if (index != 0) // The driver is reading DPCD for an external display return retVal; @@ -775,8 +774,7 @@ void IGFX::populateP0P1P2(struct ProbeContext *context) { context->kdiv = p2; } -int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) -{ +int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) { // // Abstract // @@ -812,8 +810,6 @@ int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPa // - 2019.06 // - static_assert(__offsetof(CRTCParams, pdiv) == 0x20, "Invalid pdiv offset, please check your compiler."); - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Called with pixel clock = %d Hz.", pixelClock); /// All possible dividers @@ -1031,9 +1027,8 @@ IOReturn IGFX::LSPCON::wakeUpNativeAUX() { IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { if (callbackIGFX->verboseI2C) { - auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - auto index = idxnum != nullptr ? idxnum->unsigned32BitValue() : -1; - //->unsigned32BitValue(); + uint32_t index = 0xFF; + AppleIntelFramebufferExplorer::getIndex(framebuffer, index); SYSLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", index, address, length, intermediate, flags); IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); @@ -1046,8 +1041,8 @@ IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { if (callbackIGFX->verboseI2C) { - auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - auto index = idxnum != nullptr ? idxnum->unsigned32BitValue() : -1; + uint32_t index = 0xFF; + AppleIntelFramebufferExplorer::getIndex(framebuffer, index); SYSLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", index, address, length, intermediate); IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); @@ -1171,12 +1166,11 @@ IOReturn IGFX::advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void void IGFX::framebufferSetupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath) { // Retrieve the framebuffer index - auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - if (idxnum == nullptr) { + uint32_t index; + if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) { SYSLOG("igfx", "SC: fbSetupLSPCON() Error: Failed to retrieve the framebuffer index."); return; } - auto index = idxnum->unsigned32BitValue(); DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); // Retrieve the user preference diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 23e1f0ba..74dbe72b 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -522,6 +522,9 @@ class IGFX { /// The rest fields are not of interest }; + static_assert(offsetof(CRTCParams, pdiv) == 0x20, "Invalid pdiv offset, please check your compiler."); + static_assert(sizeof(CRTCParams) == 56, "Invalid size of CRTCParams struct, please check your compiler."); + /** * Represents the current context of probing dividers for HDMI connections */ @@ -595,6 +598,28 @@ class IGFX { */ static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); + /** + * Explore the framebuffer structure in Apple's Intel graphics driver + */ + struct AppleIntelFramebufferExplorer { + /** + * [Convenient] Retrieve the framebuffer index + * + * @param framebuffer An `AppleIntelFramebuffer` instance + * @param index The framebuffer index on return + * @return `true` on success, `false` if the index does not exist. + */ + static bool getIndex(IORegistryEntry *framebuffer, uint32_t &index) { + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); + if (idxnum != nullptr) { + index = idxnum->unsigned32BitValue(); + return true; + } else { + return false; + } + } + }; + /** * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 * @@ -767,12 +792,12 @@ class IGFX { */ static LSPCON *create(void *controller, IORegistryEntry *framebuffer, void *displayPath) { // Guard: Framebuffer index should exist - auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); - if (idxnum == nullptr) { + uint32_t index; + if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) return nullptr; - } + // Call the private constructor - return new LSPCON(controller, framebuffer, displayPath, idxnum->unsigned32BitValue()); + return new LSPCON(controller, framebuffer, displayPath, index); } /** @@ -780,10 +805,7 @@ class IGFX { * * @param instance A nullable LSPCON driver instance */ - static void deleter(LSPCON *instance) { - if (instance != nullptr) - delete instance; - } + static void deleter(LSPCON *instance NONNULL) { delete instance; } /** * Probe the onboard LSPCON chip From eeb69c5f7325dbf93fcf44e192596d8f630928d3 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 25 Jun 2019 15:33:44 +0800 Subject: [PATCH 024/102] User should use DEBUG version to enable verbose I2C-over-AUX output. --- WhateverGreen/kern_igfx.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 8075a598..26c8d37d 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1029,10 +1029,10 @@ IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void if (callbackIGFX->verboseI2C) { uint32_t index = 0xFF; AppleIntelFramebufferExplorer::getIndex(framebuffer, index); - SYSLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", + DBGLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", index, address, length, intermediate, flags); IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); - SYSLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); + DBGLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); return retVal; } else { return callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); @@ -1043,10 +1043,10 @@ IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, voi if (callbackIGFX->verboseI2C) { uint32_t index = 0xFF; AppleIntelFramebufferExplorer::getIndex(framebuffer, index); - SYSLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", + DBGLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", index, address, length, intermediate); IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); - SYSLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); + DBGLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); return retVal; } else { return callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); From 4fed192a36a7d42fc054aac4d954704273d82ae0 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 6 Jul 2019 20:01:47 +0800 Subject: [PATCH 025/102] Remove the property `LSPCON::isActive` and always read the latest mode from the adapter as some LSPCON adapters are automatically switched back to LS mode on power off (i.e. Cable removal, sleep/wake up cycle). --- WhateverGreen/kern_igfx.cpp | 5 +---- WhateverGreen/kern_igfx.hpp | 27 +++++++-------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 26c8d37d..46d19c11 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -935,9 +935,7 @@ IOReturn IGFX::LSPCON::probe() { // Parse the current adapter mode Mode mode = parseMode(info->lspconCurrentMode); DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] The current adapter mode is %s.", index, getModeString(mode)); - if (mode == Mode::ProtocolConverter) - isActive = true; - else if (mode != Mode::LevelShifter) + if (mode == Mode::Invalid) SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); return kIOReturnSuccess; } @@ -995,7 +993,6 @@ IOReturn IGFX::LSPCON::setMode(Mode newMode) { // Guard: The new mode is effective now if (mode == newMode) { DBGLOG("igfx", "SC: LSPCON::setMode() DInfo: [FB%d] The new mode is now effective.", index); - isActive = newMode == Mode::ProtocolConverter; return kIOReturnSuccess; } timeout -= 20; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 74dbe72b..513b12e1 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -810,9 +810,7 @@ class IGFX { /** * Probe the onboard LSPCON chip * - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method will mark the adapter active if it has found that the adpater is already in `PCON` mode. - * @see `isActive` and `isAdapterActive()`. + * @return `kIOReturnSuccess` on success, errors otherwise */ IOReturn probe(); @@ -830,7 +828,6 @@ class IGFX { * @param newMode The new adapter mode * @return `kIOReturnSuccess` on success, errors otherwise. * @note This method will not return until `newMode` is effective. - * @note This method will mark the adapter active if `newMode` is `PCON`and becomes effective. * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. */ IOReturn setMode(Mode newMode); @@ -852,23 +849,18 @@ class IGFX { */ IOReturn wakeUpNativeAUX(); - /** - * Return `true` if the adapter is active - */ - inline bool isAdapterActive() { - return isActive; - } - /** * Return `true` if the adapter is running in the given mode * * @param mode The expected mode; one of `LS` and `PCON` */ inline bool isRunningInMode(Mode mode) { - // isActive 1 1 0 0 - // Preferred 1 0 1 0 - // XOR 0 1 1 0 - return !(isActive ^ getModeValue(mode)); + Mode currentMode; + if (getMode(¤tMode) != kIOReturnSuccess) { + DBGLOG("igfx", "LSPCON::isRunningInMode() Error: [FB%d] Failed to get the current adapter mode.", index); + return false; + } + return mode == currentMode; } private: @@ -917,10 +909,6 @@ class IGFX { /// The corresponding opaque display path instance void *displayPath; - /// Indicate whether the LSPCON adapter is active or not; - /// The adapter is considered to be active once it is switched to PCON mode - bool isActive; - /// The framebuffer index (for debugging purposes) uint32_t index; @@ -937,7 +925,6 @@ class IGFX { this->controller = controller; this->framebuffer = framebuffer; this->displayPath = displayPath; - this->isActive = false; this->index = index; } From 0f84b4102d5408a9b3d672ca665c53b032799d77 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 6 Jul 2019 20:09:07 +0800 Subject: [PATCH 026/102] Update the change log. --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7c423c4b..43cf1135 100755 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,8 @@ WhateverGreen Changelog ======================= +#### v1.3.1 +- Fixed an issue that LSPCON driver fails to set the mode after the adapter power is off, i.e. sleep/wake up cycle. + #### v1.3.0 - Fixed custom connector support for Radeon GPUs, thx @lwfitzgerald - Added `disable-gfx-submit` property to back `ngfxsubmit=0` boot argument From ca7368a4eee5c0b63b33243a6f3ee62cf3987eb8 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 9 Feb 2020 23:07:25 -0800 Subject: [PATCH 027/102] Fix the maximum link rate value in the extended DPCD buffer as well. (e.g. Dell Inspiration 7590 with Sharp display) --- WhateverGreen/kern_igfx.cpp | 4 ++-- WhateverGreen/kern_igfx.hpp | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 634b50c9..c634f367 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -755,10 +755,10 @@ IOReturn IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t ad // Guard: Check the DPCD register address // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) - if (address != 0) + if (address != DPCD_DEFAULT_ADDRESS && address != DPCD_EXTENDED_ADDRESS) return retVal; - // The driver tries to read the first 16 bytes from DPCD + // The driver tries to read the first 16 bytes from DPCD (0x0000) or extended DPCD (0x2200) // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) // We read the value of "IOFBDependentIndex" instead of accessing that field directly uint32_t index; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 666cd787..7780d86e 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -452,6 +452,16 @@ class IGFX { * Driver-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 write attempt at system start. */ uint32_t driverBacklightFrequency {}; + + /** + * The default DPCD address + */ + static constexpr uint32_t DPCD_DEFAULT_ADDRESS = 0x0000; + + /** + * The extended DPCD address + */ + static constexpr uint32_t DPCD_EXTENDED_ADDRESS = 0x2200; /** * Represents the first 16 fields of the receiver capabilities defined in DPCD From 5d4f0a0e7618df8c16d16bff56e1548815b4d68a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 9 Feb 2020 23:18:58 -0800 Subject: [PATCH 028/102] Update the manual for the new max link rate fix. --- Manual/FAQ.IntelHD.cn.md | 1 + Manual/FAQ.IntelHD.en.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index b0e41748..64398168 100755 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1739,6 +1739,7 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) 为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 +从 1.3.7 版本开始,此补丁同时修正从屏幕扩展属性里读取的错误速率值问题以解决在 Dell 灵越 7590 系列等新款笔记本上内核崩溃的问题。 ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) 另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 73114cae..24078bcb 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -1668,6 +1668,8 @@ Or instead of this property, use the boot-arg `-wegnoegpu` ## Fix the invalid maximum link rate issue on some laptops (Dell XPS 15 9570, etc.) Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. Or instead of this property, use the boot-arg `-igfxmlr`. +Starting from v1.3.7, it also fixes the invalid max link rate value read from the extended DPCD buffer. +This fixes the kernel panic on new laptops, such as Dell Inspiron 7590 with Sharp display. ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. From 75eacc23ed6f604430601aa087088708387c27fc Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 9 Feb 2020 23:33:43 -0800 Subject: [PATCH 029/102] Update the changelog. --- Changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.md b/Changelog.md index e8d167f4..2f78b0af 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,8 @@ WhateverGreen Changelog ======================= +#### v1.3.7 +- Improved the maximum link rate fix: Now correct the value read from extended DPCD as well. (by @0xFireWolf) + #### v1.3.6 - Enabled CoreLSKD streaming patches by default for AMD hardware DRM on Ivy Bridge - Repurposed 64 bit for FP 2.x streaming hardware accelerated streaming patches (can be used as `shikigva=80`) From 53e322df6ae4c2b37db1a1a2115f853745183644 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Fri, 21 Aug 2020 19:35:35 -0700 Subject: [PATCH 030/102] Merge from upstream: Resolve conflicts. --- Manual/FAQ.IntelHD.en.md | 2 +- WhateverGreen/kern_igfx.hpp | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 0cb3c718..3a499e1b 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2382,7 +2382,7 @@ Or instead of this property, use the boot-arg `-wegnoegpu` Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. Or instead of this property, use the boot-arg `-igfxmlr`. Starting from v1.3.7, it also fixes the invalid max link rate value read from the extended DPCD buffer. This fixes the kernel panic on new laptops, such as Dell Inspiron 7590 with Sharp display. ![dpcd_mlr](./Img/dpcd_mlr.png) -You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). If an invalid value is specified or property `dpcd-max-link-rate` is not specified, will be used default value `0x14`. +You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). If an invalid value is specified or property `dpcd-max-link-rate` is not specified, the driver will use the default value `0x14`. ## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on SKL, KBL and CFL platforms diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index d4996e99..22053e1d 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -583,16 +583,6 @@ class IGFX { */ uint32_t driverBacklightFrequency {}; - /** - * The default DPCD address - */ - static constexpr uint32_t DPCD_DEFAULT_ADDRESS = 0x0000; - - /** - * The extended DPCD address - */ - static constexpr uint32_t DPCD_EXTENDED_ADDRESS = 0x2200; - /** * The default DPCD address */ From eb038d2308a78165da5387822a1815db8da1b1e0 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 02:38:31 -0700 Subject: [PATCH 031/102] CDC: Add support for valid yet unsupported Core Display Clock (CDCLK) frequencies on ICL platforms to avoid the kernel panic. --- WhateverGreen/kern_igfx.cpp | 132 ++++++++++++++++++++++++++ WhateverGreen/kern_igfx.hpp | 180 +++++++++++++++++++++++++++++++++++- 2 files changed, 311 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index a79ba2d7..e8227f7a 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -281,6 +281,12 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if (!maxLinkRatePatch) maxLinkRatePatch = info->videoBuiltin->getProperty("enable-dpcd-max-link-rate-fix") != nullptr; + // Enable the Core Display Clock patch on ICL platforms + coreDisplayClockPatch = checkKernelArgument("-igfxcdc"); + // Or if `enable-cdclk-frequency-fix` is set in IGPU property + if (!coreDisplayClockPatch) + coreDisplayClockPatch = info->videoBuiltin->getProperty("enable-cdclk-frequency-fix") != nullptr; + disableAccel = checkKernelArgument("-igfxvesa"); disableTypeCCheck &= !checkKernelArgument("-igfxtypec"); @@ -375,6 +381,8 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (supportLSPCON) return true; + if (coreDisplayClockPatch) + return true; if (forceCompleteModeset.enable) return true; if (forceOnlineDisplay.enable) @@ -562,6 +570,32 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a patcher.clearError(); } } + + if (coreDisplayClockPatch) { + auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); + auto dcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); + auto scdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); + auto rr32Address = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); + if (pcdcAddress && rr32Address && dcdcAddress && scdcAddress) { + patcher.eraseCoverageInstPrefix(pcdcAddress); + patcher.eraseCoverageInstPrefix(rr32Address); + patcher.eraseCoverageInstPrefix(dcdcAddress); + patcher.eraseCoverageInstPrefix(scdcAddress); + orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); + orgDisableCDClock = reinterpret_cast(dcdcAddress); + orgSetCDClockFrequency = reinterpret_cast(scdcAddress); + orgIclReadRegister32 = reinterpret_cast(rr32Address); + if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { + DBGLOG("igfx", "CDC: Functions have been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "CDC: Failed to route functions."); + } + } else { + SYSLOG("igfx", "CDC: Failed to find symbols."); + patcher.clearError(); + } + } if (forceCompleteModeset.enable) { const char *sym = "__ZN31AppleIntelFramebufferController16hwRegsNeedUpdateEP21AppleIntelFramebufferP21AppleIntelDisplayPathPNS_10CRTCParamsEPK29IODetailedTimingInformationV2"; @@ -1587,6 +1621,104 @@ IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *d return retVal; } +void IGFX::sanitizeCDClockFrequency(void *that) +{ + // Read the hardware reference frequency from the DSSM register + auto referenceFrequency = callbackIGFX->orgIclReadRegister32(that, ICL_REG_DSSM); + + // Frequency of Core Display Clock PLL is determined by the reference frequency + uint32_t newCdclkFrequency = 0; + uint32_t newPLLFrequency = 0; + switch (referenceFrequency) { + case ICL_REF_CLOCK_FREQ_19_2: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 19.2 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_652_8; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_19_2; + break; + + case ICL_CDCLK_PLL_FREQ_REF_24_0: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 24.0 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_648_0; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_24_0; + break; + + case ICL_CDCLK_PLL_FREQ_REF_38_4: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_652_8; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_38_4; + break; + + default: + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() Error: Reference frequency is invalid. Will panic later."); + return; + } + + // Debug: Print the new frequencies + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to %s MHz.", + coreDisplayClockDecimalFrequency2String(newCdclkFrequency)); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to %u Hz.", newPLLFrequency); + + // Disable the Core Display Clock PLL + callbackIGFX->orgDisableCDClock(that); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL has been disabled."); + + // Set the new PLL frequency and reenable the Core Display Clock PLL + callbackIGFX->orgSetCDClockFrequency(that, newPLLFrequency); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock has been reprogrammed and PLL has been re-enabled."); + + // "Verify" that the new frequency is effective + auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is %s MHz now.", + coreDisplayClockDecimalFrequency2String(cdclk)); +} + +uint32_t IGFX::wrapProbeCDClockFrequency(void *that) { + // + // Abstract + // + // 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 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, such as 172.8 MHz, + // and hence a kernel panic occurs to indicate the precondition failure. + // + // This reverse engineering research analyzes functions related to configuring the Core Display Clock + // and exploits existing APIs to add support for those valid yet non-supported clock frequencies. + // Since there are multiple spots in the graphics driver that heavily rely on the above assumption, + // `AppleIntelFramebufferController::probeCDClockFrequency()` is wrapped to adjust the frequency if necessary. + // + // If the current Core Display Clock frequency is not natively supported by the driver, + // this patch will reprogram the clock to set its frequency to a supported value that is appropriate for the hardware. + // As such, the kernel panic can be avoided, and the built-in display can be lit up successfully. + // + // If you are interested in the story behind this fix, please take a look at my blog post. + // https://www.firewolf.science/2020/08/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/ + // + // - FireWolf + // - 2020.08 + // + + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Called with controller at 0x%llx.", that); + + // Read the Core Display Clock frequency from the CDCLK_CTL register + // Bit 0 - 11 stores the decimal frequency + auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is %s MHz.", + coreDisplayClockDecimalFrequency2String(cdclk)); + + // Guard: Check whether the current frequency is supported by the graphics driver + if (cdclk < ICL_CDCLK_DEC_FREQ_THRESHOLD) { + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is not supported."); + callbackIGFX->sanitizeCDClockFrequency(that); + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The core display clock has been switched to a supported frequency."); + } + + // Invoke the original method to ensure everything works as expected + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Will invoke the original function."); + auto retVal = callbackIGFX->orgProbeCDClockFrequency(that); + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x%llx.", retVal); + return retVal; +} + void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { if (reg == BXT_BLC_PWM_FREQ1) { if (value && value != callbackIGFX->driverBacklightFrequency) { diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 22053e1d..fb476692 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -296,6 +296,7 @@ class IGFX { */ uint32_t (*orgCflReadRegister32)(void *, uint32_t) {nullptr}; uint32_t (*orgKblReadRegister32)(void *, uint32_t) {nullptr}; + uint32_t (*orgIclReadRegister32)(void *, uint32_t) {nullptr}; /** * Original AppleIntelFramebufferController::WriteRegister32 function @@ -321,8 +322,30 @@ class IGFX { /** * Original AppleIntelFramebufferController::GetDPCDInfo function */ - IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *); + IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *) {nullptr}; + /** + * Original AppleIntelFramebufferController::probeCDClockFrequency function (ICL) + */ + uint32_t (*orgProbeCDClockFrequency)(void *) {nullptr}; + + /** + * Original AppleIntelFramebufferController::disableCDClock function (ICL) + */ + void (*orgDisableCDClock)(void *) {nullptr}; + + /** + * Original AppleIntelFramebufferController::setCDClockFrequency function (ICL) + * + * @param frequency The Core Display Clock PLL frequency in Hz + */ + void (*orgSetCDClockFrequency)(void *, unsigned long long) {nullptr}; + + /** + * Patch the Core Display Clock frequency if ncessary + */ + bool coreDisplayClockPatch {false}; + /** * Set to true if a black screen ComputeLaneCount patch is required */ @@ -583,6 +606,146 @@ class IGFX { */ uint32_t driverBacklightFrequency {}; + /** + * Address of the register used to retrieve the current Core Display Clock frequency + */ + static constexpr uint32_t ICL_REG_CDCLK_CTL = 0x46000; + + /** + * Address of the register used to retrieve the hardware reference clock frequency + */ + static constexpr uint32_t ICL_REG_DSSM = 0x51004; + + /** + * Enumerates all possible hardware reference clock frequencies on ICL platforms + * + * Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c + * Command Reference: Registers Part 1 – Registers A through L, DSSM Register + */ + enum ICLReferenceClockFrequency + { + // 24 MHz + ICL_REF_CLOCK_FREQ_24_0 = 0x0, + + // 19.2 MHz + ICL_REF_CLOCK_FREQ_19_2 = 0x1, + + // 38.4 MHz + ICL_REF_CLOCK_FREQ_38_4 = 0x2 + }; + + /** + * Enumerates all possible Core Display Clock decimal frequency + * + * Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c + * Command Reference: Registers Part 1 – Registers A through L, CDCLK_CTL Register + */ + enum ICLCoreDisplayClockDecimalFrequency + { + // 172.8 MHz + ICL_CDCLK_FREQ_172_8 = 0x158, + + // 180 MHz + ICL_CDCLK_FREQ_180_0 = 0x166, + + // 192 MHz + ICL_CDCLK_FREQ_192_0 = 0x17E, + + // 307.2 MHz + ICL_CDCLK_FREQ_307_2 = 0x264, + + // 312 MHz + ICL_CDCLK_FREQ_312_0 = 0x26E, + + // 552 MHz + ICL_CDCLK_FREQ_552_0 = 0x44E, + + // 556.8 MHz + ICL_CDCLK_FREQ_556_8 = 0x458, + + // 648 MHz + ICL_CDCLK_FREQ_648_0 = 0x50E, + + // 652.8 MHz + ICL_CDCLK_FREQ_652_8 = 0x518 + }; + + /** + * Get the string representation of the given Core Display Clock decimal frequency + */ + static inline const char* coreDisplayClockDecimalFrequency2String(uint32_t frequency) + { + switch (frequency) + { + case ICL_CDCLK_FREQ_172_8: + return "172.8"; + + case ICL_CDCLK_FREQ_180_0: + return "180"; + + case ICL_CDCLK_FREQ_192_0: + return "192"; + + case ICL_CDCLK_FREQ_307_2: + return "307.2"; + + case ICL_CDCLK_FREQ_312_0: + return "312"; + + case ICL_CDCLK_FREQ_552_0: + return "552"; + + case ICL_CDCLK_FREQ_556_8: + return "556.8"; + + case ICL_CDCLK_FREQ_648_0: + return "648"; + + case ICL_CDCLK_FREQ_652_8: + return "652.8"; + + default: + return "INVALID"; + } + } + + /** + * Any Core Display Clock frequency lower than this value is not supported by the driver + * + * @note This threshold is derived from the ICL framebuffer driver on macOS 10.15.6. + */ + static constexpr uint32_t ICL_CDCLK_DEC_FREQ_THRESHOLD = ICL_CDCLK_FREQ_648_0; + + /** + * Core Display Clock PLL frequency in Hz for the 24 MHz hardware reference frequency + * + * Main Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 12 Display Engine + * Page 171, CDCLK PLL Ratio and Divider Programming and Resulting Frequencies. + * + * Side Reference: + * - Intel Graphics Driver for Linux (5.8.3) bxt_calc_cdclk_pll_vco() in intel_cdclk.c + * + * @note 54 is the PLL ratio when the reference frequency is 24 MHz + */ + static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_24_0 = 24000000 * 54; + + /** + * Core Display Clock PLL frequency in Hz for the 19.2 MHz hardware reference frequency + * + * @note 68 is the PLL ratio when the reference frequency is 19.2 MHz + */ + static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_19_2 = 19200000 * 68; + + /** + * Core Display Clock PLL frequency in Hz for the 38.4 MHz hardware reference frequency + * + * @note 34 is the PLL ratio when the reference frequency is 19.2 MHz + */ + static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_38_4 = 38400000 * 34; + /** * The default DPCD address */ @@ -1360,6 +1523,21 @@ class IGFX { * Used to inject code to initialize the driver for the onboard LSPCON chip. */ static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); + + /** + * [Helper] A helper to change the Core Display Clock frequency to a supported value + */ + static void sanitizeCDClockFrequency(void *that); + + /** + * [Wrapper] Probe and adjust the Core Display Clock frequency if necessary + * + * @param that The hidden implicit `this` pointer + * @return The PLL VCO frequency in Hz derived from the current Core Display Clock frequency. + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::probeCDClockFrequency()` method. + * Used to inject code to reprogram the clock so that its frequency is natively supported by the driver. + */ + static uint32_t wrapProbeCDClockFrequency(void *that); /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates From 048b05ea281d252ef9b74a9054a0aca053e4a6b3 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 02:56:27 -0700 Subject: [PATCH 032/102] CDC: Fix an issue that the reference frequency is not properly decoded. --- WhateverGreen/kern_igfx.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index e8227f7a..254f1df9 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1624,7 +1624,8 @@ IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *d void IGFX::sanitizeCDClockFrequency(void *that) { // Read the hardware reference frequency from the DSSM register - auto referenceFrequency = callbackIGFX->orgIclReadRegister32(that, ICL_REG_DSSM); + // Bits 29-31 store the reference frequency value + auto referenceFrequency = callbackIGFX->orgIclReadRegister32(that, ICL_REG_DSSM) >> 29; // Frequency of Core Display Clock PLL is determined by the reference frequency uint32_t newCdclkFrequency = 0; From 5345c441e7ec9432b68c3c925f9c42a56d4ed1e3 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 03:14:26 -0700 Subject: [PATCH 033/102] CDC: Fix a typo in the switch statement. --- WhateverGreen/kern_igfx.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 254f1df9..f6d0a840 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1637,13 +1637,13 @@ void IGFX::sanitizeCDClockFrequency(void *that) newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_19_2; break; - case ICL_CDCLK_PLL_FREQ_REF_24_0: + case ICL_REF_CLOCK_FREQ_24_0: DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 24.0 MHz."); newCdclkFrequency = ICL_CDCLK_FREQ_648_0; newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_24_0; break; - case ICL_CDCLK_PLL_FREQ_REF_38_4: + case ICL_REF_CLOCK_FREQ_38_4: DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz."); newCdclkFrequency = ICL_CDCLK_FREQ_652_8; newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_38_4; From 7e892e2b914effc177acedcfdc948eeff256ec7d Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 03:47:56 -0700 Subject: [PATCH 034/102] CDC: Update the README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac662d0d..28598f67 100755 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ WhateverGreen - Fixes choppy video playback on Intel Kaby Lake and newer. - Fixes black screen on Intel HD since 10.15.5. - Adds workaround for rare force wake timeout panics on Intel KBL and CFL. +- Support all valid Core Display Clock (CDCLK) freqencies on Intel ICL platforms. #### Documentation @@ -84,13 +85,14 @@ not in the list, the driver's logic is used to determine whether complete modese indices of connectors for which online status is enforced. Format is similar to `igfxfcmsfbs`. - `wegtree=1` boot argument (`rebuild-device-tree` property) to force device renaming on Apple FW. - `igfxrpsc=1` boot argument (`rps-control` property) to enable RPS control patch (improves IGPU performance). +- `-igfxcdc` boot argument (`enable-cdclk-frequency-fix` property) to support all valid Core Display Clock (CDCLK) frequencies on ICL platforms. [Read the manual](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.IntelHD.en.md) #### Credits - [Apple](https://www.apple.com) for macOS - [AMD](https://www.amd.com) for ATOM VBIOS parsing code - [The PCI ID Repository](http://pci-ids.ucw.cz) for multiple GPU model names -- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix, infinite loop fix for Intel HDMI connections and LSPCON driver support +- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix, infinite loop fix for Intel HDMI connections, LSPCON driver support, and Core Display Clock frequency fix for ICL platforms - [Floris497](https://github.com/Floris497) for the CoreDisplay [patches](https://github.com/Floris497/mac-pixel-clock-patch-v2) - [Fraxul](https://github.com/Fraxul) for original CFL backlight patch - [headkaze](https://github.com/headkaze) for Intel framebuffer patching code and CFL backlight patch improvements From fc3b40acd869948974da30a7b89454a5430b524e Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 03:48:21 -0700 Subject: [PATCH 035/102] CDC: Update the change log. --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index c7576d01..1fbcc5cb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ WhateverGreen Changelog - Fixed `disable-external-gpu` (`-wegnoegpu`) on some systems - Disabled RPS control patch by default due to a bug in 10.15.6 IGPU drivers - Replaced `igfxnorpsc=1` with `igfxrpsc=1` to opt-in RPS control patch +- Support all valid Core Display Clock (CDCLK) frequencies to avoid the kernel panic of "Unsupported CD clock decimal frequency" on Intel ICL platforms. (by @0xFireWolf) #### v1.4.1 - Added `igfxmetal=1` boot argument (and `enable-metal` property) to enable Metal on offline IGPU From cfdb4ff733978a5f32d580e7125beae93d7aca5b Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 03:50:20 -0700 Subject: [PATCH 036/102] CDC: Update the manual. --- Manual/FAQ.IntelHD.cn.md | 29 +++++++++++++++++++++++++++++ Manual/FAQ.IntelHD.en.md | 29 +++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 4584381a..fa283851 100755 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1834,6 +1834,35 @@ igfx @ (DBG) SC: GetDPCDInfo() DInfo: [FB2] Returns 0x0. ![](Img/lspcon_debug.png) +## 修复 Ice Lake 平台上因 Core Display Clock (CDCLK) 频率过低而导致的内核崩溃问题 + +为核显添加 `enable-cdclk-frequency-fix` 属性或者直接使用 `-igfxcdc` 启动参数以解决 Core Display Clock (CDCLK) 频率过低而导致的内核崩溃问题。 + +核显的显示引擎是由这个 Core Display Clock 时钟来驱动的。苹果的显卡驱动假定 BIOS 或者固件已设定好时钟频率为 652.8 MHz 或者 648 MHz,而有些 Ice Lake 笔记本在开机时 BIOS 自动设定频率为最低的 172.8 MHz,所以会触发频率检查的函数而导致内核崩溃。内核崩溃后,你能看到类似 "Unsupported CD clock decimal frequency 0x158" 这样的错误信息。 + +这个补丁通过重新设定 Core Display Clock 的频率来通过上述检查以避免内核崩溃。补丁生效后,时钟速率会被设为一个苹果支持的值,具体是哪个值取决于你的硬件。补丁会基于你当前硬件的配置选择一个最佳的频率。 + +

+调试 +补丁生效后,你会在内核日志中发现类似下面的字眼。在调整前,时钟速率为 172.8 MHz,而在调整后速率变为 652.8 MHz。 + +``` +igfx: @ (DBG) CDC: Functions have been routed successfully. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: Called with controller at 0xffffff8035933000. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is 172.8 MHz. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is not supported. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to 652.8 MHz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to 1305600000 Hz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL has been disabled. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock has been reprogrammed and PLL has been re-enabled. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is 652.8 MHz now. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The core display clock has been switched to a supported frequency. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: Will invoke the original function. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x4dd1e000. +``` +
+ ## 已知问题 *兼容性*: diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 3a499e1b..275694bc 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2384,8 +2384,6 @@ Starting from v1.3.7, it also fixes the invalid max link rate value read from th ![dpcd_mlr](./Img/dpcd_mlr.png) You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). If an invalid value is specified or property `dpcd-max-link-rate` is not specified, the driver will use the default value `0x14`. -## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on SKL, KBL and CFL platforms - ## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on Skylake, Kaby Lake and Coffee Lake platforms Add the `enable-hdmi-dividers-fix` property to `IGPU` or use the `-igfxhdmidivs` boot argument instead to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. @@ -2463,6 +2461,33 @@ Additionally, you can find these properties injected by the driver under the cor ![lspcon_debug](./Img/lspcon_debug.png) +## Support all possible Core Display Clock (CDCLK) frequencies on ICL platforms + +Add the `enable-cdclk-frequency-fix` property to `IGPU` or use the `-igfxcdc` boot argument instead to support all valid Core Display Clock (CDCLK) frequencies on ICL platforms, otherwise a kernel panic would happen due to an unsupported CD clock decimal 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 firmware has already set the clock frequency to either 652.8 MHz or 648 MHz (the actual value depends on hardware), but quite a few laptops set it to a much lower value, such as 172.8 MHz, and hence you will see a kernel panic message like "Unsupported CD clock decimal frequency 0x158". This patch reprograms the clock to set its frequency to one of supported value, so that this precondition can be satisifed. + +
+Spoiler: Debugging +Once you have enabled the patch, dump your kernel log and you should also be able to see something simillar to lines below. + +``` +igfx: @ (DBG) CDC: Functions have been routed successfully. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: Called with controller at 0xffffff8035933000. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is 172.8 MHz. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is not supported. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to 652.8 MHz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to 1305600000 Hz. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL has been disabled. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock has been reprogrammed and PLL has been re-enabled. +igfx: @ (DBG) CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is 652.8 MHz now. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The core display clock has been switched to a supported frequency. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: Will invoke the original function. +igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x4dd1e000. +``` +
+ ## Known Issues **Compatibility** From b3d3b8fbcc1d03001168916c5e4e941d63c467c3 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 04:24:02 -0700 Subject: [PATCH 037/102] CDC: Fix a typo. --- WhateverGreen/kern_igfx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index f6d0a840..7b80d070 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1703,7 +1703,7 @@ uint32_t IGFX::wrapProbeCDClockFrequency(void *that) { // Read the Core Display Clock frequency from the CDCLK_CTL register // Bit 0 - 11 stores the decimal frequency auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is %s MHz.", + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The current core display clock frequency is %s MHz.", coreDisplayClockDecimalFrequency2String(cdclk)); // Guard: Check whether the current frequency is supported by the graphics driver From 5b72c992a03ed83f98bfba8b016399e89b798081 Mon Sep 17 00:00:00 2001 From: FireWolf <0xFireWolf@users.noreply.github.com> Date: Sun, 23 Aug 2020 04:58:28 -0700 Subject: [PATCH 038/102] CDC: Always print the clock frequency before and after sanitized. --- README.md | 2 +- WhateverGreen/kern_igfx.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 28598f67..6f927ef8 100755 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ WhateverGreen - Fixes choppy video playback on Intel Kaby Lake and newer. - Fixes black screen on Intel HD since 10.15.5. - Adds workaround for rare force wake timeout panics on Intel KBL and CFL. -- Support all valid Core Display Clock (CDCLK) freqencies on Intel ICL platforms. +- Supports all valid Core Display Clock (CDCLK) freqencies on Intel ICL platforms. #### Documentation diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 7b80d070..56b55e75 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1669,7 +1669,7 @@ void IGFX::sanitizeCDClockFrequency(void *that) // "Verify" that the new frequency is effective auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is %s MHz now.", + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is %s MHz now.", coreDisplayClockDecimalFrequency2String(cdclk)); } @@ -1703,7 +1703,7 @@ uint32_t IGFX::wrapProbeCDClockFrequency(void *that) { // Read the Core Display Clock frequency from the CDCLK_CTL register // Bit 0 - 11 stores the decimal frequency auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The current core display clock frequency is %s MHz.", + SYSLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The current core display clock frequency is %s MHz.", coreDisplayClockDecimalFrequency2String(cdclk)); // Guard: Check whether the current frequency is supported by the graphics driver From 048117ed29d24c230cbb743e79ecfaa51a295bb3 Mon Sep 17 00:00:00 2001 From: FireWolf <0xFireWolf@users.noreply.github.com> Date: Sun, 23 Aug 2020 05:04:06 -0700 Subject: [PATCH 039/102] CDC: Fix the analyzer warning. --- WhateverGreen/kern_igfx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 56b55e75..1d21aeae 100755 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -1655,7 +1655,7 @@ void IGFX::sanitizeCDClockFrequency(void *that) } // Debug: Print the new frequencies - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to %s MHz.", + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to %s MHz.", coreDisplayClockDecimalFrequency2String(newCdclkFrequency)); DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to %u Hz.", newPLLFrequency); From c7a932076efe1d8bbe7eb5d61651c589877db425 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 07:20:27 -0700 Subject: [PATCH 040/102] CDC: Leftover: No need to erase the prefix for functions that are no longer routed for debugging. --- WhateverGreen/kern_igfx.cpp | 3 --- 1 file changed, 3 deletions(-) mode change 100755 => 100644 WhateverGreen/kern_igfx.cpp diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp old mode 100755 new mode 100644 index 7b80d070..f731232f --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -578,9 +578,6 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a auto rr32Address = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); if (pcdcAddress && rr32Address && dcdcAddress && scdcAddress) { patcher.eraseCoverageInstPrefix(pcdcAddress); - patcher.eraseCoverageInstPrefix(rr32Address); - patcher.eraseCoverageInstPrefix(dcdcAddress); - patcher.eraseCoverageInstPrefix(scdcAddress); orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); orgDisableCDClock = reinterpret_cast(dcdcAddress); orgSetCDClockFrequency = reinterpret_cast(scdcAddress); From a91e22f33b292566d1ee5c81a718ddb0ce7fb33f Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 23 Aug 2020 07:25:28 -0700 Subject: [PATCH 041/102] CDC: Use the shared ReadRegister32() function. --- WhateverGreen/kern_igfx.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index f731232f..8119911b 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -486,9 +486,9 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a bool bklCoffeeFb = realFramebuffer == &kextIntelCFLFb && cflBacklightPatch != CoffeeBacklightPatch::Off; // Accept Kaby FB and enable backlight patches if On (Auto is irrelevant here). bool bklKabyFb = realFramebuffer == &kextIntelKBLFb && cflBacklightPatch == CoffeeBacklightPatch::On; - // Solve ReadRegister32 just once as it is sahred + // Solve ReadRegister32 just once as it is shared if (bklCoffeeFb || bklKabyFb || - RPSControl.enabled || ForceWakeWorkaround.enabled) { + RPSControl.enabled || ForceWakeWorkaround.enabled || coreDisplayClockPatch) { AppleIntelFramebufferController__ReadRegister32 = patcher.solveSymbol (index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); if (!AppleIntelFramebufferController__ReadRegister32) @@ -575,13 +575,12 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); auto dcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); auto scdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); - auto rr32Address = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); - if (pcdcAddress && rr32Address && dcdcAddress && scdcAddress) { + if (pcdcAddress && dcdcAddress && scdcAddress && AppleIntelFramebufferController__ReadRegister32) { patcher.eraseCoverageInstPrefix(pcdcAddress); orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); orgDisableCDClock = reinterpret_cast(dcdcAddress); orgSetCDClockFrequency = reinterpret_cast(scdcAddress); - orgIclReadRegister32 = reinterpret_cast(rr32Address); + orgIclReadRegister32 = AppleIntelFramebufferController__ReadRegister32; if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { DBGLOG("igfx", "CDC: Functions have been routed successfully."); } else { From 7b47afec6e85ad014d05189fb1ffb8cd674ee042 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 19:40:11 -0700 Subject: [PATCH 042/102] DVMT: Fix the DVMT calculation issue on ICL platforms. --- WhateverGreen.xcodeproj/project.pbxproj | 4 + WhateverGreen/kern_igfx.cpp | 13 ++ WhateverGreen/kern_igfx.hpp | 77 ++++++++++ WhateverGreen/kern_igfx_memory.cpp | 193 ++++++++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 WhateverGreen/kern_igfx_memory.cpp diff --git a/WhateverGreen.xcodeproj/project.pbxproj b/WhateverGreen.xcodeproj/project.pbxproj index 87e9a43f..6e2a4f6e 100644 --- a/WhateverGreen.xcodeproj/project.pbxproj +++ b/WhateverGreen.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ CEC0863624331E9B00F5B701 /* kern_agdc.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC0863524331E9B00F5B701 /* kern_agdc.hpp */; }; CEC8E2F020F765E700D3CA3A /* kern_cdf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */; }; CEC8E2F120F765E700D3CA3A /* kern_cdf.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */; }; + D5C32F5624FC45D30078A824 /* kern_igfx_memory.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */; }; E2BE6CE220FB209400ED2D55 /* kern_fb.hpp in Headers */ = {isa = PBXBuildFile; fileRef = E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */; }; /* End PBXBuildFile section */ @@ -130,6 +131,7 @@ CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_cdf.cpp; sourceTree = ""; }; CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = kern_cdf.hpp; sourceTree = ""; }; CEEF190A239CFDB1005B3BE8 /* FAQ.Chart.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FAQ.Chart.md; sourceTree = ""; }; + D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_memory.cpp; sourceTree = ""; }; E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = kern_fb.hpp; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,6 +191,7 @@ CE7FC0AC20F5622700138088 /* kern_igfx.cpp */, CE7FC0AD20F5622700138088 /* kern_igfx.hpp */, CE1F61B82432DEE800201DF4 /* kern_igfx_debug.cpp */, + D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */, 2F30012324A00F2800C590C3 /* kern_igfx_pm.cpp */, CE7FC0A820F55E7400138088 /* kern_ngfx.cpp */, CE7FC0A920F55E7400138088 /* kern_ngfx.hpp */, @@ -442,6 +445,7 @@ CEA03B5E20EE825A00BA842F /* kern_weg.cpp in Sources */, CE8190A21F1E3ECE00DE95F4 /* kern_model.cpp in Sources */, CE1970FF21C380DF00B02AB4 /* kern_nvhda.cpp in Sources */, + D5C32F5624FC45D30078A824 /* kern_igfx_memory.cpp in Sources */, CE405ED91E4A080700AA0B3D /* plugin_start.cpp in Sources */, CE7FC0CB20F682A300138088 /* kern_resources.cpp in Sources */, CE1F61B92432DEE800201DF4 /* kern_igfx_debug.cpp in Sources */, diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 9b499f96..a6aebb8f 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -287,6 +287,9 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if (!coreDisplayClockPatch) coreDisplayClockPatch = info->videoBuiltin->getProperty("enable-cdclk-frequency-fix") != nullptr; + // Example of redirecting the request to each submodule + modDVMTCalcFix.processKernel(patcher, info); + disableAccel = checkKernelArgument("-igfxvesa"); disableTypeCCheck &= !checkKernelArgument("-igfxtypec"); @@ -383,6 +386,10 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (coreDisplayClockPatch) return true; + // Similarly, if IGFX maintains a sequence of submodules, + // we could iterate through each submodule and performs OR operations. + if (modDVMTCalcFix.requiresPatchingFramebuffer) + return true; if (forceCompleteModeset.enable) return true; if (forceOnlineDisplay.enable) @@ -399,6 +406,8 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { }; auto requiresGraphicsPatches = [this]() { + if (modDVMTCalcFix.requiresPatchingGraphics) + return true; if (pavpDisablePatch) return true; if (forceOpenGL) @@ -593,6 +602,10 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } } + // We could iterate through each submodule and redirect the request if and only if the submodule is enabled + if (modDVMTCalcFix.enabled) + modDVMTCalcFix.processKext(patcher, index, address, size); + if (forceCompleteModeset.enable) { const char *sym = "__ZN31AppleIntelFramebufferController16hwRegsNeedUpdateEP21AppleIntelFramebufferP21AppleIntelDisplayPathPNS_10CRTCParamsEPK29IODetailedTimingInformationV2"; if (forceCompleteModeset.legacy) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index fb476692..2e926f15 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -499,6 +499,83 @@ class IGFX { static bool forceWakeWaitAckFallback(uint32_t, uint32_t, uint32_t); static void forceWake(void*, uint8_t set, uint32_t dom, uint32_t); } ForceWakeWorkaround; + + /** + * Interface of a submodule to fix Intel graphics drivers + */ + class PatchSubmodule { + public: + /** + * Virtual destructor + */ + virtual ~PatchSubmodule() = default; + + /** + * True if this submodule should be enabled + */ + bool enabled {false}; + + /** + * True if this submodule requires patching the framebuffer driver + */ + bool requiresPatchingFramebuffer {false}; + + /** + * True if this submodule requires patching the graphics acceleration driver + */ + bool requiresPatchingGraphics {false}; + + /** + * Initialize any data structure required by this submodule if necessary + */ + virtual void init() = 0; + + /** + * Release any resources obtained by this submodule if necessary + */ + virtual void deinit() = 0; + + /** + * Setup the fix and retrieve the device information if necessary + * + * @param patcher KernelPatcher instance + * @param info Information about the graphics device + * @note This function is called when the main IGFX module processes the kernel. + */ + virtual void processKernel(KernelPatcher &patcher, DeviceInfo *info) = 0; + + /** + * Process the kext, retrieve and/or route functions if necessary + * + * @param patcher KernelPatcher instance + * @param index kinfo handle + * @param address kinfo load address + * @param size kinfo memory size + * @note This funbction is called when the main IGFX module processes the kext. + */ + virtual void processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; + }; + + /** + * A submodule to fix the calculation of DVMT preallocated memory on ICL+ platforms + */ + struct DVMTCalcFix: public PatchSubmodule { + /** + * True if this fix is available for the current Intel platform + */ + bool available {false}; + + /** + * The amount of DVMT preallocated memory in bytes set in the BIOS + */ + uint32_t dvmt {0}; + + // MARK: Patch Submodule IMP + void init() override; + void deinit() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modDVMTCalcFix; /** * Ensure each modeset is a complete modeset. diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp new file mode 100644 index 00000000..4108d9ce --- /dev/null +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -0,0 +1,193 @@ +// +// kern_igfx_memory.cpp +// WhateverGreen +// +// Created by FireWolf on 8/30/20. +// Copyright © 2020 vit9696. All rights reserved. +// + +#include "kern_igfx.hpp" +#include +#include + +void IGFX::DVMTCalcFix::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::DVMTCalcFix::deinit() {} + +void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable the fix if designated boot argument or device property is found + enabled = checkKernelArgument("-igfxdvmt"); + if (!enabled) + enabled = info->videoBuiltin->getProperty("enable-dvmt-calc-fix") != nullptr; + + // Read the DVMT preallocated memory set in BIOS from the GMCH Graphics Control field at 0x50 (PCI0,2,0) + // TODO: Lilu needs to be updated to define the enumeration case `kIOPCIConfigGraphicsControl` + auto gms = WIOKit::readPCIConfigValue(info->videoBuiltin, /*WIOKit::kIOPCIConfigGraphicsControl*/ 0x50, 0, 16) >> 8; + + // Disable the fix if the GMS value can be calculated by Apple's formula correctly + // Reference: 10th Generation Intel Processor Families: Datasheet, Volume 2, Section 4.1.28 + // https://www.intel.com/content/www/us/en/products/docs/processors/core/10th-gen-core-families-datasheet-vol-2.html + if (gms < 0x10) { + dvmt = gms * 32; + enabled = false; + DBGLOG("igfx", "DVMT: GMS value is supported by the driver. The fix has been disabled."); + } else if (gms == 0x20 || gms == 0x30 || gms == 0x40) { + dvmt = gms * 32; + } else if (gms >= 0xF0 && gms <= 0xFE) { + dvmt = ((gms & 0x0F) + 1) * 4; + } else { + SYSLOG("igfx", "DVMT: GMS value is reserved. Check your BIOS settings. DVMT will be set to 0."); + return; + } + + DBGLOG("igfx", "DVMT: GMS value is 0x%02x; DVMT pre-allocated memory is %d MB.", gms, dvmt); + info->videoBuiltin->setProperty("fw-dvmt-gms-field-value", gms, 8); + info->videoBuiltin->setProperty("fw-dvmt-preallocated-memory", dvmt, 32); + dvmt *= (1024 * 1024); /* MB to Bytes */ +} + +void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + // Find the address of `AppleIntelFramebufferController::FBMemMgr_Init()` + auto startAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13FBMemMgr_InitEv", address, size); + if (!startAddress) { + SYSLOG("igfx", "DVMT: Failed to find FBMemMgr_Init()."); + patcher.clearError(); + return; + } + + // + // Locate Apple's formula: (GGCRegValue << 0x11) & 0xFE000000 + // + // macOS Catalina 10.15.6 (19G2021) + // loc_3f61f: movl $0x50, %esi + // loc_3f624: call __ZN11IOPCIDevice20extendedConfigRead16Ey + // loc_3f629: shll $0x11, %eax + // loc_3f62c: andl $0xFE000000, %eax + // loc_3f631: movl %eax, 0xd7c(%r15) + // The final result is saved into the framebuffer controller instance and is used later in FBMemMgr_Init(). + // + // Abstract: + // + // The amount of DVMT pre-allocated memory is a pain on laptops. + // Apple’s graphics driver reads the value set in the BIOS or the firmware + // and uses a “magic” formula to calculate the amount of memory in bytes. + // Unfortunately, the formula only works for a pre-allocated memory size that is a multiple of 32MB. + // i.e. 0M, 32M, 64M, ..., 512M. (The corresponding GMS values are 0x00, 0x01, ..., 0x10.) + // Non-Apple laptops normally set DVMT to 32MB on CFL- platforms, and related kernel panics can be solved by patching the framebuffer properly. + // + // However, problem arises as new laptops now have DVMT set to 60MB on ICL+ platforms by default, + // and the framebuffer controller ends up with initializing the stolen memory manager with an incorrect amount of pre-allocated memory. + // Later, the accelerator driver fails to allocate objects on the stolen memory area, + // and hence a kernel panic is triggered inside the `IGAccelTask::withOptions()` function. + // + // Side Note: + // Apple has removed the kernel panic from `AppleIntelFramebufferController::FBMemMgr_Init()` if the stolen memory is insufficient. (ICL) + // As a result, one will encounter a kernel panic saying "Unsupported ICL SKU" triggered in `IntelAccelerator::getGPUInfo()`. + // We could manually fix this issue, but later a page fault occurs inside `IGAccelTask::withOptions()`. + // This is because the accelerator driver fails to allocate objects on the stolen memory. + // The pointer to the object is set to NULL, and the cleanup procedure `IOAccelTask::release()` tries to dereference the NULL object. + // + // Even though one might be able to modify DVMT settings via `EFI shell` or `RU.EFI`, + // these methods are not applicable to some laptops, such as Surface Pro 7, that use custom firmware. + // As such, this patch locates instructions related to calculating the amount of memory available and identifies which register stores the final result. + // It calculates the correct number of bytes beforehand and finds the proper instruction to copy the value to the result register. + // Consequently, the framebuffer controller will initialize the memory manager with proper values and aforementioned kernel panics can be avoided. + // + // - FireWolf + // - 2020.08 + // + hde64s handle; + uint64_t shllAddr = 0, andlAddr = 0; /* Instruction Address */ + uint32_t shllSize = 0, andlSize = 0; /* Instruction Length */ + uint32_t shllDstr = 0, andlDstr = 0; /* Destination Register */ + + // e.g. movl $0x03C00000, %eax (60MB DVMT) + // Apply the middle 5 bytes if the target register is %eax; + // Apply the last 6 bytes if the target register is below %r8d; + // Apply all 7 bytes if the target register is or above %r8d. + uint8_t movl[] = {0x41, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x90}; + uint8_t nops[] = {0x90, 0x90, 0x90, 0x90}; + + // Patch Heuristics: + // We need to locate two instructions `shll` and `andl` that manipulate the value read from the GGC field, + // but we cannot assume that they always stay together and operate on the register %eax. + // As such, we need to first figure out which register is of interest, and + // we need 5 or 6 bytes to move a 32-bit integer to the register manipulated by the above instructions. + // Since `andl` is 5 - 7 bytes long, we could just replace it with a "movl" and then erases `shll` by filling `nop`s. + for (auto index = 0; index < 64; index += 1) { + auto size = Disassembler::hdeDisasm(startAddress, &handle); + + // Guard: Should be able to disassemble the function + if (handle.flags & F_ERROR) { + SYSLOG("igfx", "DVMT: Failed to disassemble FBMemMgr_Init()."); + break; + } + + // Instruction: shll $0x11, %??? + // 3 bytes long if DSTReg < %r8d, otherwise 4 bytes long + if (handle.opcode == 0xC1 && handle.imm.imm8 == 0x11) { + shllAddr = startAddress; + shllSize = handle.len; + shllDstr = (handle.rex_b << 3) | handle.modrm_rm; + DBGLOG("igfx", "DVMT: Found the shll instruction. Length = %d; DSTReg = %d.", shllSize, shllDstr); + } + + // Instruction: andl $0xFE000000, %??? + // 5 bytes long if DSTReg is %eax; 6 bytes long if DSTReg < %r8d; otherwise 7 bytes long. + if ((handle.opcode == 0x25 || handle.opcode == 0x81) && handle.imm.imm32 == 0xFE000000) { + andlAddr = startAddress; + andlSize = handle.len; + andlDstr = (handle.rex_b << 3) | handle.modrm_rm; + DBGLOG("igfx", "DVMT: Found the andl instruction. Length = %d; DSTReg = %d.", andlSize, andlDstr); + } + + // Guard: Calculate and apply the binary patch if we have found both instructions + if (shllAddr && andlAddr) { + // Update the `movl` instruction with the actual amount of DVMT preallocated memory + *(uint32_t*) (movl + 2) = dvmt; + + // Update the `movl` instruction with the actual destination register + // Find the actual starting point of the patch and the number of bytes to patch + uint8_t* patchStart; + uint32_t patchSize; + if (andlDstr >= 8) { + // %r8d, %r9d, ..., %r15d + movl[1] += (andlDstr - 8); + patchStart = movl; + patchSize = 7; + } else { + // %eax, %ecx, ..., %edi + movl[1] += andlDstr; + patchStart = (movl + 1); + patchSize = andlDstr == 0 ? /* %eax */ 5 : /* others */ 6; + } + + // Guard: Prepare to apply the binary patch + if (MachInfo::setKernelWriting(true, KernelPatcher::kernelWriteLock) != KERN_SUCCESS) { + SYSLOG("igfx", "DVMT: Failed to set kernel writing. Aborted patching."); + return; + } + + // Replace `shll` with `nop`s + // The number of nops is determined by the actual instruction length + lilu_os_memcpy(reinterpret_cast(shllAddr), nops, shllSize); + + // Replace `andl` with `movl` + // The patch contents and size are determined by the destination register of `andl` + lilu_os_memcpy(reinterpret_cast(andlAddr), patchStart, patchSize); + + // Finished applying the binary patch + MachInfo::setKernelWriting(false, KernelPatcher::kernelWriteLock); + DBGLOG("igfx", "DVMT: Calculation patch has been applied successfully."); + return; + } + + startAddress += size; + } + + SYSLOG("igfx", "DVMT: Failed to find instructions of interest. Aborted patching."); +} From e25465b5422717e8b04688a8749088e5738045ca Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 19:43:55 -0700 Subject: [PATCH 043/102] DVMT: Update the README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f927ef8..82916e25 100755 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ WhateverGreen - Fixes black screen on Intel HD since 10.15.5. - Adds workaround for rare force wake timeout panics on Intel KBL and CFL. - Supports all valid Core Display Clock (CDCLK) freqencies on Intel ICL platforms. +- Fixes the kernel panic caused by an incorrectly calculated amount of DVMT pre-allocated memory on Intel ICL platforms. #### Documentation @@ -86,13 +87,14 @@ indices of connectors for which online status is enforced. Format is similar to - `wegtree=1` boot argument (`rebuild-device-tree` property) to force device renaming on Apple FW. - `igfxrpsc=1` boot argument (`rps-control` property) to enable RPS control patch (improves IGPU performance). - `-igfxcdc` boot argument (`enable-cdclk-frequency-fix` property) to support all valid Core Display Clock (CDCLK) frequencies on ICL platforms. [Read the manual](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/FAQ.IntelHD.en.md) +- `-igfxdvmt` boot argument (`enable-dvmt-calc-fix` property) to fix the kernel panic caused by an incorrectly calculated amount of DVMT pre-allocated memory on Intel ICL platforms. #### Credits - [Apple](https://www.apple.com) for macOS - [AMD](https://www.amd.com) for ATOM VBIOS parsing code - [The PCI ID Repository](http://pci-ids.ucw.cz) for multiple GPU model names -- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix, infinite loop fix for Intel HDMI connections, LSPCON driver support, and Core Display Clock frequency fix for ICL platforms +- [FireWolf](https://github.com/0xFireWolf/) for the DPCD maximum link rate fix, infinite loop fix for Intel HDMI connections, LSPCON driver support, Core Display Clock frequency fix for ICL platforms, and DVMT pre-allocated memory calculation fix for ICL platforms. - [Floris497](https://github.com/Floris497) for the CoreDisplay [patches](https://github.com/Floris497/mac-pixel-clock-patch-v2) - [Fraxul](https://github.com/Fraxul) for original CFL backlight patch - [headkaze](https://github.com/headkaze) for Intel framebuffer patching code and CFL backlight patch improvements From 455926949956d3dbb7136d55d4f4f287499cb4da Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 20:20:37 -0700 Subject: [PATCH 044/102] DVMT: Update the manual and change log. --- Changelog.md | 1 + Manual/FAQ.IntelHD.cn.md | 25 +++++++++++++++++++++++++ Manual/FAQ.IntelHD.en.md | 21 +++++++++++++++++++++ Manual/Img/dvmt.png | Bin 0 -> 111560 bytes 4 files changed, 47 insertions(+) create mode 100644 Manual/Img/dvmt.png diff --git a/Changelog.md b/Changelog.md index 1fbcc5cb..d54c9c3f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ WhateverGreen Changelog - Disabled RPS control patch by default due to a bug in 10.15.6 IGPU drivers - Replaced `igfxnorpsc=1` with `igfxrpsc=1` to opt-in RPS control patch - Support all valid Core Display Clock (CDCLK) frequencies to avoid the kernel panic of "Unsupported CD clock decimal frequency" on Intel ICL platforms. (by @0xFireWolf) +- Fix the kernel panic caused by an incorrectly calculated amount of DVMT pre-allocated memory on Intel ICL platforms. (by @0xFireWolf) #### v1.4.1 - Added `igfxmetal=1` boot argument (and `enable-metal` property) to enable Metal on offline IGPU diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index fa283851..661e468c 100755 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1863,6 +1863,31 @@ igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The original function returns ``` +## 修复 Ice Lake 平台上因驱动错误地计算 DVMT 预分配内存大小而导致的内核崩溃问题 + +为核显添加 `enable-dvmt-calc-fix` 属性或者直接使用 `-igfxdvmt` 启动参数以修复因核显驱动错误地计算当前 DVMT 预分配内存的实际大小而导致后期加速器驱动提示 `Unsupported ICL SKU` 错误并崩溃的问题。 + +苹果的核显驱动在读取 BIOS 或者 UEFI 固件设定的 DVMT 预分配内存值后,用了一个公式来计算以字节为单位的实际可用内存大小。然而,这个公式只有在预分配内存值为 32MB 的整数倍时才会计算出正确的结果。Ice Lake 平台的笔记本出厂时 DVMT 一般设为了 60MB,所以核显驱动无法正确地初始化内存管理器。即使部分用户可以通过 `EFI Shell` 或者 `RU.EFI` 等特殊手段来修改 BIOS 中设定的 DVMT 预分配内存值,但有些使用特定固件的笔记本比如 Surface Pro 7 以及因厂商安全策略而无法修改 BIOS 设置的笔记本是无法修改 DVMT 设置的。本补丁通过预先计算当前平台的可用 DVMT 预分配内存后,将正确的数值传递给核显驱动。这样驱动可以正常初始化内存管理器,后期的因 DVMT 内存导致的崩溃问题迎刃而解。 + +苹果在 Ice Lake 的核显驱动中移出了 DVMT Stolen Memory 断言相关的崩溃语句,只会在内核日志中打印出 `Insufficient Stolen Memory`。 +请注意,虽然本补丁可让核显的内存管理器正确地初始化,我们仍然建议你给 Framebuffer 打上必要的补丁以规避上述预分配内存不足的问题。 + +此外,你可以使用 IORegistryExplorer 在 `IGPU` 下找到 `fw-dvmt-preallocated-memory` 属性来查看当前 BIOS 中设定的 DVMT 预分配内存大小。 +比如下图中的数值为 `0x3C`,对应的十进制为 `60`,即当前 DVMT 预分配内存为 60MB。 + +![](./Img/dvmt.png) + +
+调试 +补丁生效后,你会在内核日志中发现类似下面的字眼。 + +``` +igfx: @ (DBG) DVMT: Found the shll instruction. Length = 3; DSTReg = 0. +igfx: @ (DBG) DVMT: Found the andl instruction. Length = 5; DSTReg = 0. +igfx: @ (DBG) DVMT: Calculation patch has been applied successfully. +``` +
+ ## 已知问题 *兼容性*: diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 275694bc..65312488 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2488,6 +2488,27 @@ igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The original function returns ``` +## Fix the kernel panic caused by an incorrectly calculated amount of DVMT pre-allocated memory on Intel ICL platforms + +Add the `enable-dvmt-calc-fix` property to `IGPU` or use the `-igfxdvmt` boot argument instead to fix the calculation of the amount of DVMT pre-allocated memory on ICL platforms, otherwise a kernel panic saying `Unsupported ICL SKU` would happen. + +Apple’s graphics driver reads the DVMT value set in the BIOS or the firmware and uses a “magic” formula to calculate the amount of memory in bytes. Unfortunately, the formula only works for a pre-allocated memory size that is a multiple of 32MB. Problem arises as laptops now have DVMT set to 60MB on ICL+ platforms by default, and the framebuffer controller ends up with initializing the stolen memory manager with an incorrect amount of pre-allocated memory. Even though one might be able to modify DVMT settings via `EFI shell` or `RU.EFI`, these methods are not applicable to some laptops, such as Surface Pro 7, that use custom firmware. As such, this patch calculates the correct number of bytes beforehand and patches the driver so that it will initialize the memory manager with proper values and aforementioned kernel panics can be avoided. + +Apple has removed the kernel panic if the stolen memory is not enough, but you are encouraged to patch the framebuffer so that it fits into your available amount of stolen memory. Once the patch is enabled, you could find your actual amount of DVMT pre-allocated memory in the property `fw-dvmt-preallocated-memory` under the graphics device. The unit is megabyte, and the size in the example below is 60 MB. (0x3C = 60) + +![DVMT Pre-allocated Memory Size](./Img/dvmt.png) + +
+Spoiler: Debugging +Additionally, you should be able to find something similar to lines below in your kernel log. + +``` +igfx: @ (DBG) DVMT: Found the shll instruction. Length = 3; DSTReg = 0. +igfx: @ (DBG) DVMT: Found the andl instruction. Length = 5; DSTReg = 0. +igfx: @ (DBG) DVMT: Calculation patch has been applied successfully. +``` +
+ ## Known Issues **Compatibility** diff --git a/Manual/Img/dvmt.png b/Manual/Img/dvmt.png new file mode 100644 index 0000000000000000000000000000000000000000..57653374e0d85cbe5e3cd2e3ce084cb671b2ab36 GIT binary patch literal 111560 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 bGf3D_0QH* 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
yB<{Q><~y~bXTo8&+r|MazkClHs_g%L}(9T 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 B4YL3M literal 0 HcmV?d00001 From 375e8da9376eab9bf9ba9020d46d2faa7151e7aa Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 20:26:39 -0700 Subject: [PATCH 045/102] IGFX: Fix a typo. --- WhateverGreen/kern_igfx.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index a6aebb8f..69d9ade9 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -369,7 +369,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { info->videoBuiltin->setProperty("AAPL00,DualLink", dualLinkBytes, sizeof(dualLinkBytes)); } - auto requresFramebufferPatches = [this]() { + auto requiresFramebufferPatches = [this]() { if (blackScreenPatch) return true; if (applyFramebufferPatch || hdmiAutopatch) @@ -428,7 +428,7 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { }; // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !requresFramebufferPatches(); + switchOffFramebuffer = !requiresFramebufferPatches(); switchOffGraphics = !requiresGraphicsPatches(); } else { switchOffGraphics = switchOffFramebuffer = true; From 7a4c3cb5fa7b3dadb83481a2608abda841375c6e Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 20:30:39 -0700 Subject: [PATCH 046/102] DVMT: Check the availability. --- WhateverGreen/kern_igfx.cpp | 1 + WhateverGreen/kern_igfx_memory.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 69d9ade9..13c5a391 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -137,6 +137,7 @@ void IGFX::init() { currentFramebufferOpt = &kextIntelICLHPFb; forceCompleteModeset.supported = forceCompleteModeset.enable = true; disableTypeCCheck = true; + modDVMTCalcFix.available = true; break; case CPUInfo::CpuGeneration::CometLake: supportsGuCFirmware = true; diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index 4108d9ce..2a38bba0 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -19,6 +19,12 @@ void IGFX::DVMTCalcFix::init() { void IGFX::DVMTCalcFix::deinit() {} void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Guard: Disable the patch if it is not available on the current Intel platforms + if (!available) { + SYSLOG("igfx", "DVMT: This fix is not available on the current platform and has been disabled."); + return; + } + // Enable the fix if designated boot argument or device property is found enabled = checkKernelArgument("-igfxdvmt"); if (!enabled) From d49e2bdcf9917f12e819f6d664288960c77bbf94 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 20:50:07 -0700 Subject: [PATCH 047/102] DVMT: Initialize the submodule when IGFX is initialized. --- WhateverGreen/kern_igfx.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 13c5a391..51595b4f 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -61,6 +61,8 @@ IGFX *IGFX::callbackIGFX; void IGFX::init() { callbackIGFX = this; + // Initialize each submodule + modDVMTCalcFix.init(); auto &bdi = BaseDeviceInfo::get(); auto generation = bdi.cpuGeneration; auto family = bdi.cpuFamily; From 48d75f66f4d9f674777272067a0230681d3a05bc Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 21:09:22 -0700 Subject: [PATCH 048/102] DVMT: Fix the analyzer warning. --- WhateverGreen/kern_igfx_memory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index 2a38bba0..c6cedaf5 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -139,7 +139,7 @@ void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_v shllAddr = startAddress; shllSize = handle.len; shllDstr = (handle.rex_b << 3) | handle.modrm_rm; - DBGLOG("igfx", "DVMT: Found the shll instruction. Length = %d; DSTReg = %d.", shllSize, shllDstr); + SYSLOG("igfx", "DVMT: Found the shll instruction. Length = %d; DSTReg = %d.", shllSize, shllDstr); } // Instruction: andl $0xFE000000, %??? @@ -148,7 +148,7 @@ void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_v andlAddr = startAddress; andlSize = handle.len; andlDstr = (handle.rex_b << 3) | handle.modrm_rm; - DBGLOG("igfx", "DVMT: Found the andl instruction. Length = %d; DSTReg = %d.", andlSize, andlDstr); + SYSLOG("igfx", "DVMT: Found the andl instruction. Length = %d; DSTReg = %d.", andlSize, andlDstr); } // Guard: Calculate and apply the binary patch if we have found both instructions From 3abf6dff630328dbff0a4c54ce209efa4de06c6c Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 22:57:20 -0700 Subject: [PATCH 049/102] DVMT: Fix the indentation. --- WhateverGreen/kern_igfx_memory.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index c6cedaf5..69efa01c 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -135,21 +135,21 @@ void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_v // Instruction: shll $0x11, %??? // 3 bytes long if DSTReg < %r8d, otherwise 4 bytes long - if (handle.opcode == 0xC1 && handle.imm.imm8 == 0x11) { - shllAddr = startAddress; - shllSize = handle.len; + if (handle.opcode == 0xC1 && handle.imm.imm8 == 0x11) { + shllAddr = startAddress; + shllSize = handle.len; shllDstr = (handle.rex_b << 3) | handle.modrm_rm; SYSLOG("igfx", "DVMT: Found the shll instruction. Length = %d; DSTReg = %d.", shllSize, shllDstr); - } - - // Instruction: andl $0xFE000000, %??? + } + + // Instruction: andl $0xFE000000, %??? // 5 bytes long if DSTReg is %eax; 6 bytes long if DSTReg < %r8d; otherwise 7 bytes long. - if ((handle.opcode == 0x25 || handle.opcode == 0x81) && handle.imm.imm32 == 0xFE000000) { + if ((handle.opcode == 0x25 || handle.opcode == 0x81) && handle.imm.imm32 == 0xFE000000) { andlAddr = startAddress; andlSize = handle.len; andlDstr = (handle.rex_b << 3) | handle.modrm_rm; SYSLOG("igfx", "DVMT: Found the andl instruction. Length = %d; DSTReg = %d.", andlSize, andlDstr); - } + } // Guard: Calculate and apply the binary patch if we have found both instructions if (shllAddr && andlAddr) { From fba0466faba7f7dbb589181ccae662f4937c3412 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 23:01:07 -0700 Subject: [PATCH 050/102] DVMT: Set the property only in DEBUG version. --- Manual/FAQ.IntelHD.cn.md | 2 +- Manual/FAQ.IntelHD.en.md | 2 +- WhateverGreen/kern_igfx_memory.cpp | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 661e468c..18039862 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1872,7 +1872,7 @@ igfx: @ (DBG) CDC: ProbeCDClockFrequency() DInfo: The original function returns 苹果在 Ice Lake 的核显驱动中移出了 DVMT Stolen Memory 断言相关的崩溃语句,只会在内核日志中打印出 `Insufficient Stolen Memory`。 请注意,虽然本补丁可让核显的内存管理器正确地初始化,我们仍然建议你给 Framebuffer 打上必要的补丁以规避上述预分配内存不足的问题。 -此外,你可以使用 IORegistryExplorer 在 `IGPU` 下找到 `fw-dvmt-preallocated-memory` 属性来查看当前 BIOS 中设定的 DVMT 预分配内存大小。 +此外,你可以使用 IORegistryExplorer 在 `IGPU` 下找到 `fw-dvmt-preallocated-memory` 属性来查看当前 BIOS 中设定的 DVMT 预分配内存大小。(仅限 `DEBUG` 版本) 比如下图中的数值为 `0x3C`,对应的十进制为 `60`,即当前 DVMT 预分配内存为 60MB。 ![](./Img/dvmt.png) diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 65312488..0a061614 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2494,7 +2494,7 @@ Add the `enable-dvmt-calc-fix` property to `IGPU` or use the `-igfxdvmt` boot ar Apple’s graphics driver reads the DVMT value set in the BIOS or the firmware and uses a “magic” formula to calculate the amount of memory in bytes. Unfortunately, the formula only works for a pre-allocated memory size that is a multiple of 32MB. Problem arises as laptops now have DVMT set to 60MB on ICL+ platforms by default, and the framebuffer controller ends up with initializing the stolen memory manager with an incorrect amount of pre-allocated memory. Even though one might be able to modify DVMT settings via `EFI shell` or `RU.EFI`, these methods are not applicable to some laptops, such as Surface Pro 7, that use custom firmware. As such, this patch calculates the correct number of bytes beforehand and patches the driver so that it will initialize the memory manager with proper values and aforementioned kernel panics can be avoided. -Apple has removed the kernel panic if the stolen memory is not enough, but you are encouraged to patch the framebuffer so that it fits into your available amount of stolen memory. Once the patch is enabled, you could find your actual amount of DVMT pre-allocated memory in the property `fw-dvmt-preallocated-memory` under the graphics device. The unit is megabyte, and the size in the example below is 60 MB. (0x3C = 60) +Apple has removed the kernel panic if the stolen memory is not enough, but you are encouraged to patch the framebuffer so that it fits into your available amount of stolen memory. Once the patch is enabled, you could find your actual amount of DVMT pre-allocated memory in the property `fw-dvmt-preallocated-memory` under the graphics device. (Only available in DEBUG version) The unit is megabyte, and the size in the example below is 60 MB. (0x3C = 60) ![DVMT Pre-allocated Memory Size](./Img/dvmt.png) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index 69efa01c..449defa4 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -51,8 +51,10 @@ void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) } DBGLOG("igfx", "DVMT: GMS value is 0x%02x; DVMT pre-allocated memory is %d MB.", gms, dvmt); +#ifdef DEBUG info->videoBuiltin->setProperty("fw-dvmt-gms-field-value", gms, 8); info->videoBuiltin->setProperty("fw-dvmt-preallocated-memory", dvmt, 32); +#endif dvmt *= (1024 * 1024); /* MB to Bytes */ } From 23ff1fb0188d4996e367151aaa4113f94300fbbf Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 30 Aug 2020 23:05:32 -0700 Subject: [PATCH 051/102] DVMT: Wait for the device to be published in IOService plan to avoid crash on macOS Big Sur. --- WhateverGreen/kern_igfx_memory.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index 449defa4..02cd1fdc 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -30,6 +30,13 @@ void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) if (!enabled) enabled = info->videoBuiltin->getProperty("enable-dvmt-calc-fix") != nullptr; + // Guard: Wait for the device to be published in the plane, otherwise `read` will crash on macOS Big Sur + if (!WIOKit::awaitPublishing(info->videoBuiltin)) { + SYSLOG("igfx", "DVMT: Failed to wait for the graphics device to be published. The fix has been disabled."); + enabled = false; + return; + } + // Read the DVMT preallocated memory set in BIOS from the GMCH Graphics Control field at 0x50 (PCI0,2,0) // TODO: Lilu needs to be updated to define the enumeration case `kIOPCIConfigGraphicsControl` auto gms = WIOKit::readPCIConfigValue(info->videoBuiltin, /*WIOKit::kIOPCIConfigGraphicsControl*/ 0x50, 0, 16) >> 8; From 3d6d6fd5d38352381652c70a3e17ad596124a157 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 31 Aug 2020 16:51:44 -0700 Subject: [PATCH 052/102] PatchSubmodule: Separate kext processing into two functions. --- WhateverGreen/kern_igfx.hpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 2e926f15..aee85f11 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -545,7 +545,7 @@ class IGFX { virtual void processKernel(KernelPatcher &patcher, DeviceInfo *info) = 0; /** - * Process the kext, retrieve and/or route functions if necessary + * Process the framebuffer kext, retrieve and/or route functions if necessary * * @param patcher KernelPatcher instance * @param index kinfo handle @@ -553,7 +553,18 @@ class IGFX { * @param size kinfo memory size * @note This funbction is called when the main IGFX module processes the kext. */ - virtual void processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; + virtual void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; + + /** + * Process the accelerator kext, retrieve and/or route functions if necessary + * + * @param patcher KernelPatcher instance + * @param index kinfo handle + * @param address kinfo load address + * @param size kinfo memory size + * @note This funbction is called when the main IGFX module processes the kext. + */ + virtual void processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; }; /** @@ -574,7 +585,8 @@ class IGFX { void init() override; void deinit() override; void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; - void processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + void processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modDVMTCalcFix; /** From ff0fdff07e7efc43502e80be16c24092e56d3020 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 31 Aug 2020 16:52:36 -0700 Subject: [PATCH 053/102] DVMT: Avoid reading DVMT value if the patch is disabled. --- WhateverGreen/kern_igfx.cpp | 2 +- WhateverGreen/kern_igfx_memory.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 51595b4f..2ccf5ee3 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -607,7 +607,7 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a // We could iterate through each submodule and redirect the request if and only if the submodule is enabled if (modDVMTCalcFix.enabled) - modDVMTCalcFix.processKext(patcher, index, address, size); + modDVMTCalcFix.processFramebufferKext(patcher, index, address, size); if (forceCompleteModeset.enable) { const char *sym = "__ZN31AppleIntelFramebufferController16hwRegsNeedUpdateEP21AppleIntelFramebufferP21AppleIntelDisplayPathPNS_10CRTCParamsEPK29IODetailedTimingInformationV2"; diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index 02cd1fdc..b0be081b 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -29,6 +29,8 @@ void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) enabled = checkKernelArgument("-igfxdvmt"); if (!enabled) enabled = info->videoBuiltin->getProperty("enable-dvmt-calc-fix") != nullptr; + if (!enabled) + return; // Guard: Wait for the device to be published in the plane, otherwise `read` will crash on macOS Big Sur if (!WIOKit::awaitPublishing(info->videoBuiltin)) { @@ -65,7 +67,7 @@ void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) dvmt *= (1024 * 1024); /* MB to Bytes */ } -void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { +void IGFX::DVMTCalcFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { // Find the address of `AppleIntelFramebufferController::FBMemMgr_Init()` auto startAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13FBMemMgr_InitEv", address, size); if (!startAddress) { @@ -206,3 +208,5 @@ void IGFX::DVMTCalcFix::processKext(KernelPatcher &patcher, size_t index, mach_v SYSLOG("igfx", "DVMT: Failed to find instructions of interest. Aborted patching."); } + +void IGFX::DVMTCalcFix::processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {} From 3e34356aae988adaa0946d82e660696258ede370 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 1 Sep 2020 00:17:22 -0700 Subject: [PATCH 054/102] PatchSubmodule: Make interface functions non pure virtual so that a concrete type does not need to implement unnecessary functions. --- WhateverGreen/kern_igfx.hpp | 14 ++++++-------- WhateverGreen/kern_igfx_memory.cpp | 4 ---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index aee85f11..2a6ca6ee 100755 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -528,12 +528,12 @@ class IGFX { /** * Initialize any data structure required by this submodule if necessary */ - virtual void init() = 0; + virtual void init() {} /** * Release any resources obtained by this submodule if necessary */ - virtual void deinit() = 0; + virtual void deinit() {} /** * Setup the fix and retrieve the device information if necessary @@ -542,7 +542,7 @@ class IGFX { * @param info Information about the graphics device * @note This function is called when the main IGFX module processes the kernel. */ - virtual void processKernel(KernelPatcher &patcher, DeviceInfo *info) = 0; + virtual void processKernel(KernelPatcher &patcher, DeviceInfo *info) {} /** * Process the framebuffer kext, retrieve and/or route functions if necessary @@ -553,10 +553,10 @@ class IGFX { * @param size kinfo memory size * @note This funbction is called when the main IGFX module processes the kext. */ - virtual void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; + virtual void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {} /** - * Process the accelerator kext, retrieve and/or route functions if necessary + * Process the graphics accelerator kext, retrieve and/or route functions if necessary * * @param patcher KernelPatcher instance * @param index kinfo handle @@ -564,7 +564,7 @@ class IGFX { * @param size kinfo memory size * @note This funbction is called when the main IGFX module processes the kext. */ - virtual void processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) = 0; + virtual void processGraphicsKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {} }; /** @@ -583,10 +583,8 @@ class IGFX { // MARK: Patch Submodule IMP void init() override; - void deinit() override; void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; - void processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modDVMTCalcFix; /** diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index b0be081b..ee0921bb 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -16,8 +16,6 @@ void IGFX::DVMTCalcFix::init() { requiresPatchingFramebuffer = true; } -void IGFX::DVMTCalcFix::deinit() {} - void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { // Guard: Disable the patch if it is not available on the current Intel platforms if (!available) { @@ -208,5 +206,3 @@ void IGFX::DVMTCalcFix::processFramebufferKext(KernelPatcher &patcher, size_t in SYSLOG("igfx", "DVMT: Failed to find instructions of interest. Aborted patching."); } - -void IGFX::DVMTCalcFix::processAcceleratorKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {} From 93b71c3af41ab2fa4233d6110c009a95609af560 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 1 Sep 2020 00:23:30 -0700 Subject: [PATCH 055/102] DVMT: Change to C++ type cast. --- WhateverGreen/kern_igfx_memory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index ee0921bb..a508f669 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -163,7 +163,7 @@ void IGFX::DVMTCalcFix::processFramebufferKext(KernelPatcher &patcher, size_t in // Guard: Calculate and apply the binary patch if we have found both instructions if (shllAddr && andlAddr) { // Update the `movl` instruction with the actual amount of DVMT preallocated memory - *(uint32_t*) (movl + 2) = dvmt; + *reinterpret_cast(movl + 2) = dvmt; // Update the `movl` instruction with the actual destination register // Find the actual starting point of the patch and the number of bytes to patch From bb22e1f2bf7c30451c0f3c53b05a9dcab40c4f5a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 1 Sep 2020 00:25:34 -0700 Subject: [PATCH 056/102] DVMT: Add the missing `processGraphicsKext()` function call. --- WhateverGreen/kern_igfx.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 2ccf5ee3..87c7ddb9 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -486,6 +486,9 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (ForceWakeWorkaround.enabled) ForceWakeWorkaround.initGraphics(*this, patcher, index, address, size); + + if (modDVMTCalcFix.enabled) + modDVMTCalcFix.processGraphicsKext(patcher, index, address, size); return true; } From 917a70bae3168002600074a08063ef12459d2c8a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Tue, 1 Sep 2020 00:37:24 -0700 Subject: [PATCH 057/102] CI: Retrigger a check to get rid of the stuck one. --- WhateverGreen/kern_igfx_memory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index a508f669..82ed4f7e 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -206,3 +206,4 @@ void IGFX::DVMTCalcFix::processFramebufferKext(KernelPatcher &patcher, size_t in SYSLOG("igfx", "DVMT: Failed to find instructions of interest. Aborted patching."); } + From b237264d6755e85b12ebdbd51914cbbbf928ce40 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 16:13:24 -0700 Subject: [PATCH 058/102] IGFX: PatchSubmodule: Remove the virtual destructor. --- WhateverGreen/kern_igfx.hpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index fe317b82..a998941a 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -505,11 +505,6 @@ class IGFX { */ class PatchSubmodule { public: - /** - * Virtual destructor - */ - virtual ~PatchSubmodule() = default; - /** * True if this submodule should be enabled */ From ce3bef332af00410d84d21ddcc1b220288299e77 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 16:21:40 -0700 Subject: [PATCH 059/102] MLR: Refactor the max link rate fix into a submodule. --- WhateverGreen/kern_igfx.hpp | 209 ++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index a998941a..9693e85d 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -477,6 +477,11 @@ class IGFX { // Useful for getting access to Read/WriteRegister, rather than having // to compute the offsets AppleIntelFramebufferController** gFramebufferController {}; + + // Available on ICL+ + // Apple has refactored quite a large amount of code into a new class `AppleIntelPort` in the ICL graphics driver, + // and the framebuffer controller now maintains an array of `ports`. + class AppleIntelPort; struct RPSControl { bool available {false}; @@ -581,6 +586,210 @@ class IGFX { void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modDVMTCalcFix; + + /** + * A submodule to fix the maximum link rate reported by DPCD + */ + struct DPCDMaxLinkRateFix: public PatchSubmodule { + /** + * The default DPCD address that stores receiver capabilities (16 bytes) + */ + static constexpr uint32_t DPCD_DEFAULT_RECEIVER_CAPS_ADDRESS = 0x0000; + + /** + * The extended DPCD address that stores receiver capabilities (16 bytes) + */ + static constexpr uint32_t DPCD_EXTENDED_RECEIVER_CAPS_ADDRESS = 0x2200; + + /** + * The DPCD address that stores the eDP version (1 byte) + */ + static constexpr uint32_t DPCD_EDP_VERSION_ADDRESS = 0x700; + + /** + * The DPCD register value if eDP version is 1.4 + */ + static constexpr uint32_t DPCD_EDP_VERSION_1_4_VALUE = 0x03; + + /** + * The DPCD address that stores link rates supported by the eDP panel (2 bytes * 8) + */ + static constexpr uint32_t DPCD_EDP_SUPPORTED_LINK_RATES_ADDRESS = 0x010; + + /** + * The maximum number of link rates stored in the table + */ + static constexpr size_t DP_MAX_NUM_SUPPORTED_RATES = 8; + + /** + * User-specified maximum link rate value in the DPCD buffer + * + * Auto: Default value is 0x00 to detect the maximum link rate automatically; + * User: Specify a custom link rate via the `dpcd-max-link-rate` property. + */ + uint32_t maxLinkRate {0x00}; + + /** + * [CFL-] The framebuffer controller instance passed to `ReadAUX()` + * + * @note This field is set to invoke platform-independent ReadAUX(). + */ + AppleIntelFramebufferController *controller {nullptr}; + + /** + * [CFL-] The framebuffer instance passed to `ReadAUX()` + * + * @note This field is set to invoke platform-independent ReadAUX(). + */ + IORegistryEntry *framebuffer {nullptr}; + + /** + * [CFL-] The display path instance passed to `ReadAUX()` + * + * @note This field is set to invoke platform-independent ReadAUX(). + */ + void *displayPath {nullptr}; + + /** + * [ICL+] The port instance passed to `ReadAUX()` + * + * @note This field is set to invoke platform-independent ReadAUX(). + */ + AppleIntelPort *port {nullptr}; + + /** + * [CFL-] Original AppleIntelFramebufferController::ReadAUX function + * + * @seealso Refer to the document of `wrapReadAUX()` below. + */ + IOReturn (*orgCFLReadAUX)(AppleIntelFramebufferController *, IORegistryEntry *, uint32_t, uint16_t, void *, void *) {nullptr}; + + /** + * [ICL+] Original AppleIntelPort::readAUX function + * + * @seealso Refer to the document of `wrapICLReadAUX()` below. + */ + IOReturn (*orgICLReadAUX)(AppleIntelPort *, uint32_t, void *, uint32_t) {nullptr}; + + /** + * [ICL+] Original AppleIntelPort::getFBFromPort function + * + * @param that The hidden implicity framebuffer controller instance + * @param port Specify the port instance to retrieve the corresponding framebuffer instance + * @return The framebuffer instance associated with the given port on success, `NULL` otherwise. + * @note This function is required to retrieve the framebuffer index via a port. + */ + IORegistryEntry *(*orgICLGetFBFromPort)(AppleIntelFramebufferController *, AppleIntelPort *) {nullptr}; + + /** + * [CFL-] ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer + * + * @param that The hidden implicit framebuffer controller instance + * @param framebuffer The framebuffer instance + * @param address DPCD register address + * @param length Specify the number of bytes read from the register at `address` + * @param buffer A non-null buffer to store bytes read from DPCD + * @param displayPath The display path instance + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The actual work is delegated to the platform-independent function `wrapReadAUX()`. + */ + static IOReturn wrapCFLReadAUX(AppleIntelFramebufferController *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); + + /** + * [ICL+] ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer + * + * @param that The hidden implicit port instance + * @param address DPCD register address + * @param buffer A non-null buffer to store bytes read from DPCD + * @param length Specify the number of bytes read from the register at `address` + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The actual work is delegated to the platform-independent function `wrapReadAUX()`. + */ + static IOReturn wrapICLReadAUX(AppleIntelPort *that, uint32_t address, void *buffer, uint32_t length); + + /** + * [Common] ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer + * + * @param address DPCD register address + * @param buffer A non-null buffer to store bytes read from DPCD + * @param length Specify the number of bytes read from the register at `address` + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note This function is independent of the platform. + */ + static IOReturn wrapReadAUX(uint32_t address, void *buffer, uint32_t length); + + /** + * [Common] Read from DPCD via DisplayPort AUX channel + * + * @param address DPCD register address + * @param buffer A non-null buffer to store bytes read from DPCD + * @param length Specify the number of bytes read from the register at `address` + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note This function is independent of the platform. + */ + IOReturn orgReadAUX(uint32_t address, void* buffer, uint32_t length); + + /** + * [Common] Retrieve the framebuffer index + * + * @param index The framebuffer index on return + * @return `true` on success, `false` otherwise. + * @note This function is independent of the platform. + */ + bool getFramebufferIndex(uint32_t &index); + + /** + * [Helper] Verify the given link rate value + * + * @param rate The decimal link rate value + * @return The given rate value if it is supported by the driver, `0` otherwise. + */ + static inline uint32_t verifyLinkRateValue(uint32_t rate) { + switch (rate) { + case 0x1E: // HBR3 8.1 Gbps + case 0x14: // HBR2 5.4 Gbps + case 0x0C: // 3_24 3.24 Gbps Used by Apple internally + case 0x0A: // HBR 2.7 Gbps + case 0x06: // RBR 1.62 Gbps + return rate; + + default: + return 0; + } + } + + /** + * [Helper] Get the maximum link rate value from the given table read from DPCD + * + * @param table A table of link rates supported by the eDP 1.4 panel + * @return The maximum link rate value, `0` if failed to find one supported by Apple's driver. + * @note The driver only supports `0x06` (RBR), `0x0A` (HBR), `0x0C` (Apple's Internal), `0x14` (HBR2), `0x1E` (HBR3). + */ + uint32_t getMaxLinkRateFromTable(uint16_t table[DP_MAX_NUM_SUPPORTED_RATES]); + + /** + * [Helper] Probe the maximum link rate from DPCD + * + * @return The maximum link rate value on success, `0` otherwise. + * @note This function is independent of the platform. + */ + uint32_t probeMaxLinkRate(); + + /** + * [CFL-] Process the framebuffer kext for CFL- platforms + */ + void processFramebufferKextForCFL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); + + /** + * [ICL+] Process the framebuffer kext for ICL+ platforms + */ + void processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); + + // MARK: Patch Submodule IMP + void init() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modDPCDMaxLinkRateFix; /** * Ensure each modeset is a complete modeset. From 95584d0239ec222c8269eac9fe9317302ecf73a8 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:02:29 -0700 Subject: [PATCH 060/102] MLR: Merge the helper function into probe(). --- WhateverGreen/kern_igfx.hpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 9693e85d..a3965ca5 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -758,20 +758,13 @@ class IGFX { } } - /** - * [Helper] Get the maximum link rate value from the given table read from DPCD - * - * @param table A table of link rates supported by the eDP 1.4 panel - * @return The maximum link rate value, `0` if failed to find one supported by Apple's driver. - * @note The driver only supports `0x06` (RBR), `0x0A` (HBR), `0x0C` (Apple's Internal), `0x14` (HBR2), `0x1E` (HBR3). - */ - uint32_t getMaxLinkRateFromTable(uint16_t table[DP_MAX_NUM_SUPPORTED_RATES]); - /** * [Helper] Probe the maximum link rate from DPCD * * @return The maximum link rate value on success, `0` otherwise. * @note This function is independent of the platform. + * @note This function also returns `0` if the maximum link rate found in the table is not supported by Apple's driver. + * @note The driver only supports `0x06` (RBR), `0x0A` (HBR), `0x0C` (Apple's Internal), `0x14` (HBR2), `0x1E` (HBR3). */ uint32_t probeMaxLinkRate(); From 1c394e95d2fafab8eab39ca01b6921a5b1a2bcd7 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:05:55 -0700 Subject: [PATCH 061/102] IGFX: Extend the framebuffer explorer to handle the case where framebuffer is NULL. --- WhateverGreen/kern_igfx.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index a3965ca5..d3302f4d 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -1217,9 +1217,12 @@ class IGFX { * * @param framebuffer An `AppleIntelFramebuffer` instance * @param index The framebuffer index on return - * @return `true` on success, `false` if the index does not exist. + * @return `true` on success, `false` if the framebuffer is NULL or the index does not exist. */ static bool getIndex(IORegistryEntry *framebuffer, uint32_t &index) { + if (framebuffer == nullptr) + return false; + auto idxnum = OSDynamicCast(OSNumber, framebuffer->getProperty("IOFBDependentIndex")); if (idxnum != nullptr) { index = idxnum->unsigned32BitValue(); From 5b75a1d6c183958ca7f9ef91f2e8fed05a98b897 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:07:14 -0700 Subject: [PATCH 062/102] MLR: Extend the fix to probe the maximum link rate from the table and support ICL+ platforms. --- WhateverGreen.xcodeproj/project.pbxproj | 4 + WhateverGreen/kern_igfx_clock.cpp | 244 ++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 WhateverGreen/kern_igfx_clock.cpp diff --git a/WhateverGreen.xcodeproj/project.pbxproj b/WhateverGreen.xcodeproj/project.pbxproj index 6e2a4f6e..a4a8b74f 100644 --- a/WhateverGreen.xcodeproj/project.pbxproj +++ b/WhateverGreen.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ CEC0863624331E9B00F5B701 /* kern_agdc.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC0863524331E9B00F5B701 /* kern_agdc.hpp */; }; CEC8E2F020F765E700D3CA3A /* kern_cdf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */; }; CEC8E2F120F765E700D3CA3A /* kern_cdf.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */; }; + D5224EF125172B2500D5CF16 /* kern_igfx_clock.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */; }; D5C32F5624FC45D30078A824 /* kern_igfx_memory.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */; }; E2BE6CE220FB209400ED2D55 /* kern_fb.hpp in Headers */ = {isa = PBXBuildFile; fileRef = E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */; }; /* End PBXBuildFile section */ @@ -131,6 +132,7 @@ CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_cdf.cpp; sourceTree = ""; }; CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = kern_cdf.hpp; sourceTree = ""; }; CEEF190A239CFDB1005B3BE8 /* FAQ.Chart.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FAQ.Chart.md; sourceTree = ""; }; + D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_clock.cpp; sourceTree = ""; }; D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_memory.cpp; sourceTree = ""; }; E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = kern_fb.hpp; sourceTree = ""; }; /* End PBXFileReference section */ @@ -192,6 +194,7 @@ CE7FC0AD20F5622700138088 /* kern_igfx.hpp */, CE1F61B82432DEE800201DF4 /* kern_igfx_debug.cpp */, D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */, + D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */, 2F30012324A00F2800C590C3 /* kern_igfx_pm.cpp */, CE7FC0A820F55E7400138088 /* kern_ngfx.cpp */, CE7FC0A920F55E7400138088 /* kern_ngfx.hpp */, @@ -437,6 +440,7 @@ files = ( CE7FC0AE20F5622700138088 /* kern_igfx.cpp in Sources */, CEC8E2F020F765E700D3CA3A /* kern_cdf.cpp in Sources */, + D5224EF125172B2500D5CF16 /* kern_igfx_clock.cpp in Sources */, 1C9CB7B01C789FF500231E41 /* kern_rad.cpp in Sources */, CE766ED6210763B200A84567 /* kern_guc.cpp in Sources */, CE7FC0B420F6809600138088 /* kern_shiki.cpp in Sources */, diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp new file mode 100644 index 00000000..101185f2 --- /dev/null +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -0,0 +1,244 @@ +// +// kern_igfx_clock.cpp +// WhateverGreen +// +// Created by FireWolf on 9/19/20. +// Copyright © 2020 vit9696. All rights reserved. +// + +#include "kern_igfx.hpp" +#include + +/// +/// This file contains the following clock-related fixes +/// +/// 1. Maximum Link Rate fix for eDP panel on CFL+. +/// 2. Core Display Clock fix for the graphics engine on ICL+. +/// + +// MARK: - Maximum Link Rate Fix + +void IGFX::DPCDMaxLinkRateFix::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::DPCDMaxLinkRateFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable maximum link rate patch if the corresponding boot argument is found + enabled = checkKernelArgument("-igfxmlr"); + // Or if "enable-dpcd-max-link-rate-fix" is set in IGPU property + if (!enabled) + enabled = info->videoBuiltin->getProperty("enable-dpcd-max-link-rate-fix") != nullptr; + if (!enabled) + return; + + // Read the custom maximum link rate set by the user if present + if (WIOKit::getOSDataValue(info->videoBuiltin, "dpcd-max-link-rate", maxLinkRate)) { + // Guard: Verify the custom link rate before using it + if (verifyLinkRateValue(maxLinkRate) != 0) { + DBGLOG("igfx", "MLR: Found a valid custom maximum link rate value 0x%02x.", maxLinkRate); + } else { + SYSLOG("igfx", "MLR: Found an invalid custom maximum link rate value 0x%02x. Will probe the value automatically.", maxLinkRate); + maxLinkRate = 0; + } + } else { + DBGLOG("igfx", "MLR: No custom maximum link rate specified. Will probe the value automatically."); + } +} + +void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + auto raux = patcher.solveSymbol(index, "__ZN14AppleIntelPort7readAUXEjPvj", address, index); + auto gfbp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, index); + + if (raux && gfbp) { + patcher.eraseCoverageInstPrefix(raux); + orgICLReadAUX = reinterpret_cast(patcher.routeFunction(raux, reinterpret_cast(wrapICLReadAUX), true)); + orgICLGetFBFromPort = reinterpret_cast(gfbp); + if (orgICLReadAUX && orgICLGetFBFromPort) { + DBGLOG("igfx", "MLR: [ICL+] Functions have been routed successfully."); + } else { + SYSLOG("igfx", "MLR: [ICL+] Failed to route functions."); + } + } else { + SYSLOG("igfx", "MLR: [ICL+] Failed to find symbols."); + patcher.clearError(); + } +} + +void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForCFL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + auto raux = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + + if (raux) { + patcher.eraseCoverageInstPrefix(raux); + orgCFLReadAUX = reinterpret_cast(patcher.routeFunction(raux, reinterpret_cast(wrapCFLReadAUX), true)); + if (orgCFLReadAUX) { + DBGLOG("igfx", "MLR: [CFL-] Functions have been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "MLR: [CFL-] Failed to route functions."); + } + } else { + SYSLOG("igfx", "MLR: [CFL-] Failed to find symbols."); + patcher.clearError(); + } +} + +void IGFX::DPCDMaxLinkRateFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + if (BaseDeviceInfo::get().cpuGeneration >= CPUInfo::CpuGeneration::IceLake) { + DBGLOG("igfx", "MLR: Found ICL+ platforms. Will setup the fix for the ICL+ graphics driver."); + processFramebufferKextForICL(patcher, index, address, size); + } else { + DBGLOG("igfx", "MLR: Found CFL- platforms. Will setup the fix for the CFL- graphics driver."); + processFramebufferKextForCFL(patcher, index, address, size); + } +} + +IOReturn IGFX::DPCDMaxLinkRateFix::wrapCFLReadAUX(AppleIntelFramebufferController *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { + // Store required arguments for platform-independent function call + callbackIGFX->modDPCDMaxLinkRateFix.controller = that; + callbackIGFX->modDPCDMaxLinkRateFix.framebuffer = framebuffer; + callbackIGFX->modDPCDMaxLinkRateFix.displayPath = displayPath; + + // Invoke the platform-independent wrapper function + DBGLOG("igfx", "MLR: [CFL-] wrapReadAUX() Called with controller at 0x%llx and framebuffer at 0x%llx.", that, framebuffer); + return callbackIGFX->modDPCDMaxLinkRateFix.wrapReadAUX(address, buffer, length); +} + +IOReturn IGFX::DPCDMaxLinkRateFix::wrapICLReadAUX(AppleIntelPort *that, uint32_t address, void *buffer, uint32_t length) { + // Store required arguments for platform-independent function call + callbackIGFX->modDPCDMaxLinkRateFix.port = that; + + // Invoke the platform-independent wrapper function + DBGLOG("igfx", "MLR: [ICL+] wrapReadAUX() Called with port at 0x%llx.", that); + return callbackIGFX->modDPCDMaxLinkRateFix.wrapReadAUX(address, buffer, length); +} + +IOReturn IGFX::DPCDMaxLinkRateFix::wrapReadAUX(uint32_t address, void *buffer, uint32_t length) { + // + // Abstract: + // + // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of + // an invalid value of maximum link rate reported by DPCD of the builtin display. + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // **Updated in Phase 3**: + // Some panels report a maximum link rate of 0x00, but it is totally valid if the panel conforms to eDP 1.4. + // In this case, an array of supported link rates can be found at DPCD register 0x010 (Link Rate Table). + // As a result, if the laptop has an eDP 1.4 panel, we could try to probe the maximum link rate from the table. + // + // One of those fields, namely the number of lanes, is later used as a divisor during + // the link training, resulting in a kernel panic triggered by a divison-by-zero. + // + // DPCD are retrieved from the display via a helper function named ReadAUX(). + // This wrapper function checks whether the driver is reading receiver capabilities + // from DPCD of the builtin display and then provides a custom maximum link rate value, + // so that we don't need to update the binary patch on each system update. + // + // If you are interested in the story behind this fix, take a look at my blog posts. + // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ + // Phase 3: TODO: ADD LINK + + // Call the original ReadAUX() function to read from DPCD + IOReturn retVal = callbackIGFX->modDPCDMaxLinkRateFix.orgReadAUX(address, buffer, length); + + // Guard: Check the DPCD register address + // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) + if (address != DPCD_DEFAULT_ADDRESS && address != DPCD_EXTENDED_ADDRESS) + return retVal; + + // The driver tries to read the first 16 bytes from DPCD (0x0000) or extended DPCD (0x2200) + // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) + // We read the value of "IOFBDependentIndex" instead of accessing that field directly + uint32_t index; + // Guard: Should be able to retrieve the index from the registry + if (!callbackIGFX->modDPCDMaxLinkRateFix.getFramebufferIndex(index)) { + SYSLOG("igfx", "MLR: [COMM] wrapReadAUX() Failed to read the current framebuffer index."); + return retVal; + } + + // Guard: Check the framebuffer index + // By default, FB 0 refers to the builtin display + if (index != 0) + // The driver is reading DPCD for an external display + return retVal; + + // The driver tries to read the receiver capabilities for the builtin display + auto caps = reinterpret_cast(buffer); + + // Set the custom maximum link rate value if user has specified one + if (callbackIGFX->modDPCDMaxLinkRateFix.maxLinkRate != 0) { + DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Will use the maximum link rate specified by user."); + caps->maxLinkRate = callbackIGFX->modDPCDMaxLinkRateFix.maxLinkRate; + } else { + DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Will probe the maximum link rate from the table."); + caps->maxLinkRate = callbackIGFX->modDPCDMaxLinkRateFix.probeMaxLinkRate(); + } + + // All done + DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); + return retVal; +} + +IOReturn IGFX::DPCDMaxLinkRateFix::orgReadAUX(uint32_t address, void *buffer, uint32_t length) { + if (port != nullptr) { + // ICL+ + DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to ICL IMP with Address = %u; Length = %u.", address, length); + return orgICLReadAUX(port, address, buffer, length); + } else { + // CFL- + DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = %u; Length = %u.", address, length); + return orgCFLReadAUX(controller, framebuffer, address, length, buffer, displayPath); + } +} + +bool IGFX::DPCDMaxLinkRateFix::getFramebufferIndex(uint32_t &index) { + auto framebuffer = port != nullptr ? orgICLGetFBFromPort(*callbackIGFX->gFramebufferController, port) : this->framebuffer; + DBGLOG("igfx", "MLR: [COMM] GetFBIndex() Port at 0x%llx; Framebuffer at 0x%llx.", port, framebuffer); + return AppleIntelFramebufferExplorer::getIndex(framebuffer, index); +} + +uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { + // Precondition: This function is only called when the framebuffer index is 0 (i.e. builtin display) + // Guard: Read the eDP version from DPCD + uint8_t eDPVersion; + if (orgReadAUX(DPCD_EDP_VERSION_ADDRESS, &eDPVersion, 1) != kIOReturnSuccess) { + SYSLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Failed to read the eDP version. Aborted."); + return 0; + } + + // Guard: Ensure that eDP is >= 1.4 + if (eDPVersion < DPCD_EDP_VERSION_1_4_VALUE) { + SYSLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() eDP version is less than 1.4. Aborted."); + return 0; + } + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Found eDP version 1.4+ (%u).", eDPVersion); + + // Guard: Read all supported link rates + uint16_t rates[DP_MAX_NUM_SUPPORTED_RATES] = {0}; + if (orgReadAUX(DPCD_EDP_SUPPORTED_LINK_RATES_ADDRESS, rates, sizeof(rates)) != kIOReturnSuccess) { + SYSLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Failed to read supported link rates from DPCD."); + return 0; + } + + // Parse all supported link rates reported by DPCD + // The last non-zero entry in the table is the maximum link rate supported by the eDP 1.4 panel + uint32_t last = 0; + for (int index = 0; index < arrsize(rates); index += 1) { + // Guard: Table is terminated by a zero entry + if (rates[index] == 0) { + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() End of table."); + break; + } + + // Calculate the link rate value + // Each element in the table is encoded as a multiple of 200 KHz + // The decimal value (e.g. 0x14) is encoded as a multiple of 0.27 GHz (270000 KHz) + last = rates[index] * 200 / 270000; + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Table[%d] = %u; Link Rate = %llu; Decimal Value = %u.", + index, rates[index], static_cast(rates[index]) * 200 * 1000, last); + } + + // Ensure that the maximum link rate found in the table is supported by the driver + return verifyLinkRateValue(last); +} From add623edd5fc46c579ec6824c5fe6b2288bcc4da Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:40:50 -0700 Subject: [PATCH 063/102] MLR: Move the definition of receiver capabilities. --- WhateverGreen/kern_igfx_clock.cpp | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 101185f2..0ddf8e53 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -18,6 +18,47 @@ // MARK: - Maximum Link Rate Fix +/** + * Represents the first 16 fields of the receiver capabilities defined in DPCD + * + * Main Reference: + * - DisplayPort Specification Version 1.2 + * + * Side Reference: + * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) + * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h + */ +struct DPCDCap16 { // 16 bytes + // DPCD Revision (DP Config Version) + // Value: 0x10, 0x11, 0x12, 0x13, 0x14 + uint8_t revision; + + // Maximum Link Rate + // Value: 0x1E (HBR3) 8.1 Gbps + // 0x14 (HBR2) 5.4 Gbps + // 0x0C (3_24) 3.24 Gbps + // 0x0A (HBR) 2.7 Gbps + // 0x06 (RBR) 1.62 Gbps + // Reference: 0x0C is used by Apple internally. + uint8_t maxLinkRate; + + // Maximum Number of Lanes + // Value: 0x1 (HBR2) + // 0x2 (HBR) + // 0x4 (RBR) + // Side Notes: + // (1) Bit 7 is used to indicate whether the link is capable of enhanced framing. + // (2) Bit 6 is used to indicate whether TPS3 is supported. + uint8_t maxLaneCount; + + // Maximum Downspread + uint8_t maxDownspread; + + // Other fields omitted in this struct + // Detailed information can be found in the specification + uint8_t others[12]; +}; + void IGFX::DPCDMaxLinkRateFix::init() { // We only need to patch the framebuffer driver requiresPatchingGraphics = false; From d309052b0bfba73d2b5c08d216cdee73d19d39be Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:43:14 -0700 Subject: [PATCH 064/102] MLR: Cache the probe result to avoid duplicated computations; Improve the formatting for debug information. --- WhateverGreen/kern_igfx_clock.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 0ddf8e53..019fe6cd 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -209,11 +209,14 @@ IOReturn IGFX::DPCDMaxLinkRateFix::wrapReadAUX(uint32_t address, void *buffer, u // Set the custom maximum link rate value if user has specified one if (callbackIGFX->modDPCDMaxLinkRateFix.maxLinkRate != 0) { - DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Will use the maximum link rate specified by user."); + DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Will use the maximum link rate specified by user or cached by the previous probe call."); caps->maxLinkRate = callbackIGFX->modDPCDMaxLinkRateFix.maxLinkRate; } else { DBGLOG("igfx", "MLR: [COMM] wrapReadAUX() Will probe the maximum link rate from the table."); caps->maxLinkRate = callbackIGFX->modDPCDMaxLinkRateFix.probeMaxLinkRate(); + // The graphics driver tries to read the maximum link rate from both DPCD address 0x0 and 0x2200. + // We save the probe result to avoid duplicated computation. + callbackIGFX->modDPCDMaxLinkRateFix.maxLinkRate = caps->maxLinkRate; } // All done @@ -224,11 +227,11 @@ IOReturn IGFX::DPCDMaxLinkRateFix::wrapReadAUX(uint32_t address, void *buffer, u IOReturn IGFX::DPCDMaxLinkRateFix::orgReadAUX(uint32_t address, void *buffer, uint32_t length) { if (port != nullptr) { // ICL+ - DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to ICL IMP with Address = %u; Length = %u.", address, length); + DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to ICL IMP with Address = 0x%x; Length = %u.", address, length); return orgICLReadAUX(port, address, buffer, length); } else { // CFL- - DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = %u; Length = %u.", address, length); + DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x%x; Length = %u.", address, length); return orgCFLReadAUX(controller, framebuffer, address, length, buffer, displayPath); } } @@ -253,7 +256,7 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { SYSLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() eDP version is less than 1.4. Aborted."); return 0; } - DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Found eDP version 1.4+ (%u).", eDPVersion); + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Found eDP version 1.4+ (Value = 0x%x).", eDPVersion); // Guard: Read all supported link rates uint16_t rates[DP_MAX_NUM_SUPPORTED_RATES] = {0}; @@ -276,7 +279,7 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { // Each element in the table is encoded as a multiple of 200 KHz // The decimal value (e.g. 0x14) is encoded as a multiple of 0.27 GHz (270000 KHz) last = rates[index] * 200 / 270000; - DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Table[%d] = %u; Link Rate = %llu; Decimal Value = %u.", + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Table[%d] = %5u; Link Rate = %llu; Decimal Value = 0x%02x.", index, rates[index], static_cast(rates[index]) * 200 * 1000, last); } From a7202875141f4d46183fd111bdf9a015f435dec2 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 17:49:29 -0700 Subject: [PATCH 065/102] MLR: Remove the old version: Phase 1. --- WhateverGreen/kern_igfx.cpp | 28 --------------- WhateverGreen/kern_igfx.hpp | 68 ------------------------------------- 2 files changed, 96 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 87c7ddb9..4280e8c4 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -277,12 +277,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { // Enable the verbose output in I2C-over-AUX transactions if the corresponding boot argument is found verboseI2C = checkKernelArgument("-igfxi2cdbg"); - - // Enable maximum link rate patch if the corresponding boot argument is found - maxLinkRatePatch = checkKernelArgument("-igfxmlr"); - // Or if "enable-dpcd-max-link-rate-fix" is set in IGPU property - if (!maxLinkRatePatch) - maxLinkRatePatch = info->videoBuiltin->getProperty("enable-dpcd-max-link-rate-fix") != nullptr; // Enable the Core Display Clock patch on ICL platforms coreDisplayClockPatch = checkKernelArgument("-igfxcdc"); @@ -297,28 +291,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { disableTypeCCheck &= !checkKernelArgument("-igfxtypec"); - // Read the custom maximum link rate if present - if (WIOKit::getOSDataValue(info->videoBuiltin, "dpcd-max-link-rate", maxLinkRate)) { - // Guard: Verify the custom link rate before using it - switch (maxLinkRate) { - case 0x1E: // HBR3 8.1 Gbps - case 0x14: // HBR2 5.4 Gbps - case 0x0C: // 3_24 3.24 Gbps Used by Apple internally - case 0x0A: // HBR 2.7 Gbps - case 0x06: // RBR 1.62 Gbps - DBGLOG("igfx", "MLR: Found a valid custom maximum link rate value 0x%02x", maxLinkRate); - break; - - default: - // Invalid link rate value - SYSLOG("igfx", "MLR: Found an invalid custom maximum link rate value. Will use 0x14 as a fallback value."); - maxLinkRate = 0x14; - break; - } - } else { - DBGLOG("igfx", "MLR: No custom max link rate specified. Will use 0x14 as the default value."); - } - // Enable CFL backlight patch on mobile CFL or if IGPU propery enable-cfl-backlight-fix is set int bkl = 0; if (PE_parse_boot_argn("igfxcflbklt", &bkl, sizeof(bkl))) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index d3302f4d..4e972a5f 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -304,11 +304,6 @@ class IGFX { void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; - /** - * Original AppleIntelFramebufferController::ReadAUX function - */ - IOReturn (*orgReadAUX)(void *, void *, uint32_t, uint16_t, void *, void *) {nullptr}; - /** * Original AppleIntelFramebufferController::ReadI2COverAUX function */ @@ -1029,69 +1024,6 @@ class IGFX { * @note 34 is the PLL ratio when the reference frequency is 19.2 MHz */ static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_38_4 = 38400000 * 34; - - /** - * The default DPCD address - */ - static constexpr uint32_t DPCD_DEFAULT_ADDRESS = 0x0000; - - /** - * The extended DPCD address - */ - static constexpr uint32_t DPCD_EXTENDED_ADDRESS = 0x2200; - - /** - * Represents the first 16 fields of the receiver capabilities defined in DPCD - * - * Main Reference: - * - DisplayPort Specification Version 1.2 - * - * Side Reference: - * - struct intel_dp @ line 1073 in intel_drv.h (Linux 4.19 Kernel) - * - DP_RECEIVER_CAP_SIZE @ line 964 in drm_dp_helper.h - */ - struct DPCDCap16 { // 16 bytes - // DPCD Revision (DP Config Version) - // Value: 0x10, 0x11, 0x12, 0x13, 0x14 - uint8_t revision; - - // Maximum Link Rate - // Value: 0x1E (HBR3) 8.1 Gbps - // 0x14 (HBR2) 5.4 Gbps - // 0x0C (3_24) 3.24 Gbps - // 0x0A (HBR) 2.7 Gbps - // 0x06 (RBR) 1.62 Gbps - // Reference: 0x0C is used by Apple internally. - uint8_t maxLinkRate; - - // Maximum Number of Lanes - // Value: 0x1 (HBR2) - // 0x2 (HBR) - // 0x4 (RBR) - // Side Notes: - // (1) Bit 7 is used to indicate whether the link is capable of enhanced framing. - // (2) Bit 6 is used to indicate whether TPS3 is supported. - uint8_t maxLaneCount; - - // Maximum Downspread - uint8_t maxDownspread; - - // Other fields omitted in this struct - // Detailed information can be found in the specification - uint8_t others[12]; - }; - - /** - * User-specified maximum link rate value in the DPCD buffer - * - * Default value is 0x14 (5.4 Gbps, HBR2) for 4K laptop display - */ - uint32_t maxLinkRate {0x14}; - - /** - * ReadAUX wrapper to modify the maximum link rate value in the DPCD buffer - */ - static IOReturn wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath); /** * See function definition for explanation From ad81a8c44a294ad80adc1b6e63d0d2f53d76ebf9 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 18:02:07 -0700 Subject: [PATCH 066/102] MLR: Remove the old version: Phase 2. --- WhateverGreen/kern_igfx.cpp | 72 ------------------------------- WhateverGreen/kern_igfx_clock.cpp | 2 +- 2 files changed, 1 insertion(+), 73 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 4280e8c4..a9e56824 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -540,23 +540,6 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a patcher.clearError(); } } - - if (maxLinkRatePatch) { - auto readAUXAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); - if (readAUXAddress) { - patcher.eraseCoverageInstPrefix(readAUXAddress); - orgReadAUX = reinterpret_cast(patcher.routeFunction(readAUXAddress, reinterpret_cast(wrapReadAUX), true)); - if (orgReadAUX) { - DBGLOG("igfx", "MLR: ReadAUX() has been routed successfully"); - } else { - patcher.clearError(); - SYSLOG("igfx", "MLR: Failed to route ReadAUX()"); - } - } else { - SYSLOG("igfx", "MLR: Failed to find ReadAUX()"); - patcher.clearError(); - } - } if (coreDisplayClockPatch) { auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); @@ -988,61 +971,6 @@ uint32_t IGFX::wrapGetDisplayStatus(IOService *framebuffer, void *displayPath) { return ret; } -IOReturn IGFX::wrapReadAUX(void *that, IORegistryEntry *framebuffer, uint32_t address, uint16_t length, void *buffer, void *displayPath) { - - // - // Abstract: - // - // Several fields in an `AppleIntelFramebuffer` instance are left zeroed because of - // an invalid value of maximum link rate reported by DPCD of the builtin display. - // - // One of those fields, namely the number of lanes, is later used as a divisor during - // the link training, resulting in a kernel panic triggered by a divison-by-zero. - // - // DPCD are retrieved from the display via a helper function named ReadAUX(). - // This wrapper function checks whether the driver is reading receiver capabilities - // from DPCD of the builtin display and then provides a custom maximum link rate value, - // so that we don't need to update the binary patch on each system update. - // - // If you are interested in the story behind this fix, take a look at my blog posts. - // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ - // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ - - // Call the original ReadAUX() function to read from DPCD - IOReturn retVal = callbackIGFX->orgReadAUX(that, framebuffer, address, length, buffer, displayPath); - - // Guard: Check the DPCD register address - // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) - if (address != DPCD_DEFAULT_ADDRESS && address != DPCD_EXTENDED_ADDRESS) - return retVal; - - // The driver tries to read the first 16 bytes from DPCD (0x0000) or extended DPCD (0x2200) - // Get the current framebuffer index (An UInt32 field at 0x1dc in a framebuffer instance) - // We read the value of "IOFBDependentIndex" instead of accessing that field directly - uint32_t index; - // Guard: Should be able to retrieve the index from the registry - if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) { - SYSLOG("igfx", "MLR: wrapReadAUX: Failed to read the current framebuffer index."); - return retVal; - } - - // Guard: Check the framebuffer index - // By default, FB 0 refers the builtin display - if (index != 0) - // The driver is reading DPCD for an external display - return retVal; - - // The driver tries to read the receiver capabilities for the builtin display - auto caps = reinterpret_cast(buffer); - - // Set the custom maximum link rate value - caps->maxLinkRate = callbackIGFX->maxLinkRate; - - DBGLOG("igfx", "MLR: wrapReadAUX: Maximum link rate 0x%02x has been set in the DPCD buffer.", caps->maxLinkRate); - - return retVal; -} - void IGFX::populateP0P1P2(struct ProbeContext *context) { uint32_t p = context->divider; uint32_t p0 = 0; diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 019fe6cd..8cc6da6f 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -185,7 +185,7 @@ IOReturn IGFX::DPCDMaxLinkRateFix::wrapReadAUX(uint32_t address, void *buffer, u // Guard: Check the DPCD register address // The first 16 fields of the receiver capabilities reside at 0x0 (DPCD Register Address) - if (address != DPCD_DEFAULT_ADDRESS && address != DPCD_EXTENDED_ADDRESS) + if (address != DPCD_DEFAULT_RECEIVER_CAPS_ADDRESS && address != DPCD_EXTENDED_RECEIVER_CAPS_ADDRESS) return retVal; // The driver tries to read the first 16 bytes from DPCD (0x0000) or extended DPCD (0x2200) From 774dc1e089736b337871a3c72cb62e163b409420 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 18:09:44 -0700 Subject: [PATCH 067/102] MLR: Move the const register and value definition to the workspace; Setup the access level for the submodule. --- WhateverGreen/kern_igfx.hpp | 34 +++---------------------------- WhateverGreen/kern_igfx_clock.cpp | 30 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 4e972a5f..2ad5ade7 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -585,37 +585,8 @@ class IGFX { /** * A submodule to fix the maximum link rate reported by DPCD */ - struct DPCDMaxLinkRateFix: public PatchSubmodule { - /** - * The default DPCD address that stores receiver capabilities (16 bytes) - */ - static constexpr uint32_t DPCD_DEFAULT_RECEIVER_CAPS_ADDRESS = 0x0000; - - /** - * The extended DPCD address that stores receiver capabilities (16 bytes) - */ - static constexpr uint32_t DPCD_EXTENDED_RECEIVER_CAPS_ADDRESS = 0x2200; - - /** - * The DPCD address that stores the eDP version (1 byte) - */ - static constexpr uint32_t DPCD_EDP_VERSION_ADDRESS = 0x700; - - /** - * The DPCD register value if eDP version is 1.4 - */ - static constexpr uint32_t DPCD_EDP_VERSION_1_4_VALUE = 0x03; - - /** - * The DPCD address that stores link rates supported by the eDP panel (2 bytes * 8) - */ - static constexpr uint32_t DPCD_EDP_SUPPORTED_LINK_RATES_ADDRESS = 0x010; - - /** - * The maximum number of link rates stored in the table - */ - static constexpr size_t DP_MAX_NUM_SUPPORTED_RATES = 8; - + class DPCDMaxLinkRateFix: public PatchSubmodule { + private: /** * User-specified maximum link rate value in the DPCD buffer * @@ -774,6 +745,7 @@ class IGFX { void processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size); // MARK: Patch Submodule IMP + public: void init() override; void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 8cc6da6f..f9a687b7 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -18,6 +18,36 @@ // MARK: - Maximum Link Rate Fix +/** + * The default DPCD address that stores receiver capabilities (16 bytes) + */ +static constexpr uint32_t DPCD_DEFAULT_RECEIVER_CAPS_ADDRESS = 0x0000; + +/** + * The extended DPCD address that stores receiver capabilities (16 bytes) + */ +static constexpr uint32_t DPCD_EXTENDED_RECEIVER_CAPS_ADDRESS = 0x2200; + +/** + * The DPCD address that stores the eDP version (1 byte) + */ +static constexpr uint32_t DPCD_EDP_VERSION_ADDRESS = 0x700; + +/** + * The DPCD register value if eDP version is 1.4 + */ +static constexpr uint32_t DPCD_EDP_VERSION_1_4_VALUE = 0x03; + +/** + * The DPCD address that stores link rates supported by the eDP panel (2 bytes * 8) + */ +static constexpr uint32_t DPCD_EDP_SUPPORTED_LINK_RATES_ADDRESS = 0x010; + +/** + * The maximum number of link rates stored in the table + */ +static constexpr size_t DP_MAX_NUM_SUPPORTED_RATES = 8; + /** * Represents the first 16 fields of the receiver capabilities defined in DPCD * From 9bd5299c03906ed9e693d9d03cb3792a45bf81f8 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 18:13:14 -0700 Subject: [PATCH 068/102] DVMT: Setup the access level for the submodule. --- WhateverGreen/kern_igfx.hpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 2ad5ade7..50f461f3 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -565,16 +565,18 @@ class IGFX { /** * A submodule to fix the calculation of DVMT preallocated memory on ICL+ platforms */ - struct DVMTCalcFix: public PatchSubmodule { + class DVMTCalcFix: public PatchSubmodule { + private: /** - * True if this fix is available for the current Intel platform + * The amount of DVMT preallocated memory in bytes set in the BIOS */ - bool available {false}; + uint32_t dvmt {0}; + public: /** - * The amount of DVMT preallocated memory in bytes set in the BIOS + * True if this fix is available for the current Intel platform */ - uint32_t dvmt {0}; + bool available {false}; // MARK: Patch Submodule IMP void init() override; From 122dc3f9ddbaaaa7ac16651cab32d7c22d0b4ce5 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 18:39:47 -0700 Subject: [PATCH 069/102] MLR: Update the manual. --- Manual/FAQ.IntelHD.cn.md | 36 +++++++++++++++++++++++++++++++++++- Manual/FAQ.IntelHD.en.md | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index 18039862..21493d8e 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1740,11 +1740,45 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) 为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 从 1.3.7 版本开始,此补丁同时修正从屏幕扩展属性里读取的错误速率值问题以解决在 Dell 灵越 7590 系列等新款笔记本上内核崩溃的问题。 +从 1.4.3 版本开始,如果用户未定义 `dpcd-max-link-rate` 属性的话,此补丁将自动从 DPCD 寻找内屏支持的最大链路速率值。此外此补丁已适配 Ice Lake 平台。 ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) 另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 可选值为 `0x06` (RBR),`0x0A` (HBR),`0x14` (HBR2) 以及 `0x1E` (HBR3)。 -若指定了其他值,则补丁默认使用 `0x14`。若不定义此属性的话,同样默认使用 `0x14`。 +若指定了其他值,或者未定义此属性的话,则补丁默认自动寻找内屏所支持的链路最大值。 +若显卡驱动不支持找到的链路最大值的话,那么之后会触发内核崩溃,因此你需要按照上述方法手动指定一个合法的值。(这个情况理论上应该很少见。) + +
+调试 +当驱动自动寻找最大链路速率值时,你会在内核日志里发现如下的日志。 +在此例中,Dell XPS 15 9570 的 4K 内屏所支持的最大链路速率值为 5.4 Gbps,因此补丁写入对应的 `0x14` 值。 + +``` +igfx: @ (DBG) MLR: Found CFL- platforms. Will setup the fix for the CFL- graphics driver. +igfx: @ (DBG) MLR: [CFL-] Functions have been routed successfully. +igfx: @ (DBG) MLR: [CFL-] wrapReadAUX() Called with controller at 0xffffff802ca6e000 and framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x0; Length = 16. +igfx: @ (DBG) MLR: [COMM] GetFBIndex() Port at 0x0; Framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Will probe the maximum link rate from the table. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x700; Length = 1. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Found eDP version 1.4+ (Value = 0x4). +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x10; Length = 16. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[0] = 8100; Link Rate = 1620000000; Decimal Value = 0x06. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[1] = 10800; Link Rate = 2160000000; Decimal Value = 0x08. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[2] = 12150; Link Rate = 2430000000; Decimal Value = 0x09. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[3] = 13500; Link Rate = 2700000000; Decimal Value = 0x0a. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[4] = 16200; Link Rate = 3240000000; Decimal Value = 0x0c. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[5] = 21600; Link Rate = 4320000000; Decimal Value = 0x10. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[6] = 27000; Link Rate = 5400000000; Decimal Value = 0x14. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() End of table. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Maximum link rate 0x14 has been set in the DPCD buffer. +igfx: @ (DBG) MLR: [CFL-] wrapReadAUX() Called with controller at 0xffffff802ca6e000 and framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x2200; Length = 16. +igfx: @ (DBG) MLR: [COMM] GetFBIndex() Port at 0x0; Framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Will use the maximum link rate specified by user or cached by the previous probe call. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Maximum link rate 0x14 has been set in the DPCD buffer. +``` +
## 修复核显驱动在尝试点亮外接 HDMI 高分辨率显示器时造成的死循环问题 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 0a061614..8be527e9 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2381,8 +2381,43 @@ Or instead of this property, use the boot-arg `-wegnoegpu` Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. Or instead of this property, use the boot-arg `-igfxmlr`. Starting from v1.3.7, it also fixes the invalid max link rate value read from the extended DPCD buffer. This fixes the kernel panic on new laptops, such as Dell Inspiron 7590 with Sharp display. +Starting from v1.4.3, it probes the maximum link rate value automatically if the property `dpcd-max-link-rate` is not specified, and it now supports Ice Lake platforms. ![dpcd_mlr](./Img/dpcd_mlr.png) -You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). If an invalid value is specified or property `dpcd-max-link-rate` is not specified, the driver will use the default value `0x14`. +You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). +If an invalid value is specified or property `dpcd-max-link-rate` is not specified, the driver will probe the maximum link rate from DPCD instead. +If the probed value is not supported by the driver (which should rarely happen), you need to manually specify a valid one, otherwise the graphics driver will trigger a kernel panic due to a division-by-zero later. + +
+Spoiler: Debugging +When the driver probes the maximum link rate from DPCD, you should be able to see something similar to the following lines in your kernel log. +The maximum link rate reported by the 4K panel on Dell XPS 15 9570 is 5.4 Gbps, and thus the fix writes `0x14` to the DPCD buffer. + +``` +igfx: @ (DBG) MLR: Found CFL- platforms. Will setup the fix for the CFL- graphics driver. +igfx: @ (DBG) MLR: [CFL-] Functions have been routed successfully. +igfx: @ (DBG) MLR: [CFL-] wrapReadAUX() Called with controller at 0xffffff802ca6e000 and framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x0; Length = 16. +igfx: @ (DBG) MLR: [COMM] GetFBIndex() Port at 0x0; Framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Will probe the maximum link rate from the table. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x700; Length = 1. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Found eDP version 1.4+ (Value = 0x4). +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x10; Length = 16. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[0] = 8100; Link Rate = 1620000000; Decimal Value = 0x06. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[1] = 10800; Link Rate = 2160000000; Decimal Value = 0x08. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[2] = 12150; Link Rate = 2430000000; Decimal Value = 0x09. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[3] = 13500; Link Rate = 2700000000; Decimal Value = 0x0a. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[4] = 16200; Link Rate = 3240000000; Decimal Value = 0x0c. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[5] = 21600; Link Rate = 4320000000; Decimal Value = 0x10. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() Table[6] = 27000; Link Rate = 5400000000; Decimal Value = 0x14. +igfx: @ (DBG) MLR: [COMM] ProbeMaxLinkRate() End of table. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Maximum link rate 0x14 has been set in the DPCD buffer. +igfx: @ (DBG) MLR: [CFL-] wrapReadAUX() Called with controller at 0xffffff802ca6e000 and framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x2200; Length = 16. +igfx: @ (DBG) MLR: [COMM] GetFBIndex() Port at 0x0; Framebuffer at 0xffffff81aa5a3000. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Will use the maximum link rate specified by user or cached by the previous probe call. +igfx: @ (DBG) MLR: [COMM] wrapReadAUX() Maximum link rate 0x14 has been set in the DPCD buffer. +``` +
## Fix the infinite loop on establishing Intel HDMI connections with a higher pixel clock rate on Skylake, Kaby Lake and Coffee Lake platforms Add the `enable-hdmi-dividers-fix` property to `IGPU` or use the `-igfxhdmidivs` boot argument instead to fix the infinite loop when the graphics driver tries to establish a HDMI connection with a higher pixel clock rate, for example connecting to a 2K/4K display with HDMI 1.4, otherwise the system just hangs (and your builtin laptop display remains black) when you plug in the HDMI cable. From 697486e97ec8beb83be7a766c4f5913e86376714 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 18:50:04 -0700 Subject: [PATCH 070/102] MLR: Update the changelog. --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index e1d915e2..a4f62974 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ WhateverGreen Changelog - Added CFL and CML P630 - Added MacKernelSDK with Xcode 12 compatibility - Fixed loading on macOS 10.11 and earlier +- Extended the maximum link rate fix: Now probe the rate from DPCD automatically and support Intel ICL platforms. (by @0xFireWolf) #### v1.4.2 - Fixed `disable-external-gpu` (`-wegnoegpu`) on some systems From b46a143d7f0ff9b846d36d270d0a73459fbb3210 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 22:38:33 -0700 Subject: [PATCH 071/102] IGFX: Define a collection of submodules and redirect requests to each submodule. --- WhateverGreen/kern_igfx.cpp | 45 ++++++++++++++++++++++++------------- WhateverGreen/kern_igfx.hpp | 5 +++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index a9e56824..1d827916 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -62,7 +62,8 @@ IGFX *IGFX::callbackIGFX; void IGFX::init() { callbackIGFX = this; // Initialize each submodule - modDVMTCalcFix.init(); + for (auto submodule : submodules) + submodule->init(); auto &bdi = BaseDeviceInfo::get(); auto generation = bdi.cpuGeneration; auto family = bdi.cpuFamily; @@ -169,6 +170,9 @@ void IGFX::init() { } void IGFX::deinit() { + // Deinitialize each submodule + for (auto submodule : submodules) + submodule->deinit(); for (auto &con : lspcons) { LSPCON::deleter(con.lspcon); con.lspcon = nullptr; @@ -284,8 +288,9 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { if (!coreDisplayClockPatch) coreDisplayClockPatch = info->videoBuiltin->getProperty("enable-cdclk-frequency-fix") != nullptr; - // Example of redirecting the request to each submodule - modDVMTCalcFix.processKernel(patcher, info); + // Iterate through each submodule and redirect the request + for (auto submodule : submodules) + submodule->processKernel(patcher, info); disableAccel = checkKernelArgument("-igfxvesa"); @@ -343,7 +348,16 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { uint8_t dualLinkBytes[] { 0x00, 0x00, 0x00, 0x00 }; info->videoBuiltin->setProperty("AAPL00,DualLink", dualLinkBytes, sizeof(dualLinkBytes)); } - + + // Iterate through each submodule and see if we need to patch the graphics and the framebuffer kext + auto submodulesRequiresFramebufferPatch = false; + auto submodulesRequiresGraphicsPatch = false; + for (auto submodule : submodules) { + submodulesRequiresFramebufferPatch = submodulesRequiresFramebufferPatch || submodule->requiresPatchingFramebuffer; + submodulesRequiresGraphicsPatch = submodulesRequiresGraphicsPatch || submodule->requiresPatchingGraphics; + } + + // Ideally, we could get rid of these two lambda expressions auto requiresFramebufferPatches = [this]() { if (blackScreenPatch) return true; @@ -361,10 +375,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (coreDisplayClockPatch) return true; - // Similarly, if IGFX maintains a sequence of submodules, - // we could iterate through each submodule and performs OR operations. - if (modDVMTCalcFix.requiresPatchingFramebuffer) - return true; if (forceCompleteModeset.enable) return true; if (forceOnlineDisplay.enable) @@ -403,8 +413,8 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { }; // Disable kext patching if we have nothing to do. - switchOffFramebuffer = !requiresFramebufferPatches(); - switchOffGraphics = !requiresGraphicsPatches(); + switchOffFramebuffer = !requiresFramebufferPatches() && !submodulesRequiresFramebufferPatch; + switchOffGraphics = !requiresGraphicsPatches() && !submodulesRequiresGraphicsPatch; } else { switchOffGraphics = switchOffFramebuffer = true; } @@ -459,8 +469,10 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (ForceWakeWorkaround.enabled) ForceWakeWorkaround.initGraphics(*this, patcher, index, address, size); - if (modDVMTCalcFix.enabled) - modDVMTCalcFix.processGraphicsKext(patcher, index, address, size); + // Iterate through each submodule and redirect the request if and only if the submodule is enabled + for (auto submodule : submodules) + if (submodule->enabled) + submodule->processGraphicsKext(patcher, index, address, size); return true; } @@ -488,7 +500,7 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (!AppleIntelFramebufferController__WriteRegister32) SYSLOG("igfx", "Failed to find WriteRegister32"); } - if (RPSControl.enabled || ForceWakeWorkaround.enabled) + if (RPSControl.enabled || ForceWakeWorkaround.enabled || modDPCDMaxLinkRateFix.enabled) gFramebufferController = patcher.solveSymbol(index, "_gController", address, size); if (bklCoffeeFb || bklKabyFb) { // Intel backlight is modeled via pulse-width modulation (PWM). See page 144 of: @@ -563,9 +575,10 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a } } - // We could iterate through each submodule and redirect the request if and only if the submodule is enabled - if (modDVMTCalcFix.enabled) - modDVMTCalcFix.processFramebufferKext(patcher, index, address, size); + // Iterate through each submodule and redirect the request if and only if the submodule is enabled + for (auto submodule : submodules) + if (submodule->enabled) + submodule->processFramebufferKext(patcher, index, address, size); if (forceCompleteModeset.enable) { const char *sym = "__ZN31AppleIntelFramebufferController16hwRegsNeedUpdateEP21AppleIntelFramebufferP21AppleIntelDisplayPathPNS_10CRTCParamsEPK29IODetailedTimingInformationV2"; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 632f06ea..c3c40de8 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -753,6 +753,11 @@ class IGFX { void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modDPCDMaxLinkRateFix; + /** + * A collection of submodules + */ + PatchSubmodule *submodules[2] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix }; + /** * Ensure each modeset is a complete modeset. */ From 0af67012601dc4dc2b85c935d6161415f5ea62f2 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 22:49:38 -0700 Subject: [PATCH 072/102] MLR: Remove the old version: Phase 3. --- WhateverGreen/kern_igfx.cpp | 2 -- WhateverGreen/kern_igfx.hpp | 5 ----- 2 files changed, 7 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 1d827916..eeb071ad 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -367,8 +367,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (cflBacklightPatch != CoffeeBacklightPatch::Off) return true; - if (maxLinkRatePatch) - return true; if (hdmiP0P1P2Patch) return true; if (supportLSPCON) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index c3c40de8..f68ae404 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -363,11 +363,6 @@ class IGFX { */ CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; - /** - * Patch the maximum link rate in the DPCD buffer read from the built-in display - */ - bool maxLinkRatePatch {false}; - /** * Set to true to enable LSPCON driver support */ From 8d3bc2b5b43a7a38cb2390bff1b970a26201dd14 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:07:13 -0700 Subject: [PATCH 073/102] CDC: Add the patch submodule definition for the Core Display Clock fix. --- WhateverGreen/kern_igfx.hpp | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index f68ae404..d3db1b6c 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -747,6 +747,66 @@ class IGFX { void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modDPCDMaxLinkRateFix; + + /** + * A submodule to support all valid Core Display Clock frequencies on ICL+ platforms + */ + class CoreDisplayClockFix: public PatchSubmodule { + private: + /** + * [ICL+] Original AppleIntelFramebufferController::ReadRegister32 function + * + * @param that The implicit hidden framebuffer controller instance + * @param address Address of the MMIO register + * @return The 32-bit integer read from the register. + */ + uint32_t (*orgIclReadRegister32)(void *, uint32_t) {nullptr}; + + /** + * [ICL+] Original AppleIntelFramebufferController::probeCDClockFrequency function + * + * @seealso Refer to the document of `wrapProbeCDClockFrequency()` below. + */ + uint32_t (*orgProbeCDClockFrequency)(void *) {nullptr}; + + /** + * [ICL+] Original AppleIntelFramebufferController::disableCDClock function + * + * @param that The implicit hidden framebuffer controller instance + * @note This function is required to reprogram the Core Display Clock. + */ + void (*orgDisableCDClock)(void *) {nullptr}; + + /** + * [ICL+] Original AppleIntelFramebufferController::setCDClockFrequency function + * + * @param that The implicit hidden framebuffer controller instance + * @param frequency The Core Display Clock PLL frequency in Hz + * @note This function changes the frequency of the Core Display Clock and reenables it. + */ + void (*orgSetCDClockFrequency)(void *, unsigned long long) {nullptr}; + + /** + * [Helper] A helper to change the Core Display Clock frequency to a supported value + */ + static void sanitizeCDClockFrequency(void *that); + + /** + * [Wrapper] Probe and adjust the Core Display Clock frequency if necessary + * + * @param that The hidden implicit `this` pointer + * @return The PLL VCO frequency in Hz derived from the current Core Display Clock frequency. + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::probeCDClockFrequency()` method. + * Used to inject code to reprogram the clock so that its frequency is natively supported by the driver. + */ + static uint32_t wrapProbeCDClockFrequency(void *that); + + public: + // MARK: Patch Submodule IMP + void init() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modCoreDisplayClockFix; /** * A collection of submodules From 89c9e1e46c2461d540ea99efca9c7812e3b7361f Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:08:29 -0700 Subject: [PATCH 074/102] CDC: Relocate the Core Display Clock fix as a patch submodule. --- WhateverGreen/kern_igfx_clock.cpp | 137 ++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index f9a687b7..0c9dd2b3 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -316,3 +316,140 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { // Ensure that the maximum link rate found in the table is supported by the driver return verifyLinkRateValue(last); } + +// MARK: - Core Display Clock Fix + +void IGFX::CoreDisplayClockFix::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::CoreDisplayClockFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable the Core Display Clock patch on ICL platforms + enabled = checkKernelArgument("-igfxcdc"); + // Or if `enable-cdclk-frequency-fix` is set in IGPU property + if (!enabled) + enabled = info->videoBuiltin->getProperty("enable-cdclk-frequency-fix") != nullptr; +} + +void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); + auto dcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); + auto scdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); + + if (pcdcAddress && dcdcAddress && scdcAddress && callbackIGFX->AppleIntelFramebufferController__ReadRegister32) { + patcher.eraseCoverageInstPrefix(pcdcAddress); + orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); + orgDisableCDClock = reinterpret_cast(dcdcAddress); + orgSetCDClockFrequency = reinterpret_cast(scdcAddress); + orgIclReadRegister32 = callbackIGFX->AppleIntelFramebufferController__ReadRegister32; + if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { + DBGLOG("igfx", "CDC: Functions have been routed successfully."); + } else { + patcher.clearError(); + SYSLOG("igfx", "CDC: Failed to route functions."); + } + } else { + SYSLOG("igfx", "CDC: Failed to find symbols."); + patcher.clearError(); + } +} + +void IGFX::CoreDisplayClockFix::sanitizeCDClockFrequency(void *that) { + // Read the hardware reference frequency from the DSSM register + // Bits 29-31 store the reference frequency value + auto referenceFrequency = callbackIGFX->modCoreDisplayClockFix.orgIclReadRegister32(that, ICL_REG_DSSM) >> 29; + + // Frequency of Core Display Clock PLL is determined by the reference frequency + uint32_t newCdclkFrequency = 0; + uint32_t newPLLFrequency = 0; + switch (referenceFrequency) { + case ICL_REF_CLOCK_FREQ_19_2: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 19.2 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_652_8; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_19_2; + break; + + case ICL_REF_CLOCK_FREQ_24_0: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 24.0 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_648_0; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_24_0; + break; + + case ICL_REF_CLOCK_FREQ_38_4: + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz."); + newCdclkFrequency = ICL_CDCLK_FREQ_652_8; + newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_38_4; + break; + + default: + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() Error: Reference frequency is invalid. Will panic later."); + return; + } + + // Debug: Print the new frequencies + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to %s MHz.", + coreDisplayClockDecimalFrequency2String(newCdclkFrequency)); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to %u Hz.", newPLLFrequency); + + // Disable the Core Display Clock PLL + callbackIGFX->modCoreDisplayClockFix.orgDisableCDClock(that); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL has been disabled."); + + // Set the new PLL frequency and reenable the Core Display Clock PLL + callbackIGFX->modCoreDisplayClockFix.orgSetCDClockFrequency(that, newPLLFrequency); + DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock has been reprogrammed and PLL has been re-enabled."); + + // "Verify" that the new frequency is effective + auto cdclk = callbackIGFX->modCoreDisplayClockFix.orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; + SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is %s MHz now.", + coreDisplayClockDecimalFrequency2String(cdclk)); +} + +uint32_t IGFX::CoreDisplayClockFix::wrapProbeCDClockFrequency(void *that) { + // + // Abstract + // + // 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 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, such as 172.8 MHz, + // and hence a kernel panic occurs to indicate the precondition failure. + // + // This reverse engineering research analyzes functions related to configuring the Core Display Clock + // and exploits existing APIs to add support for those valid yet non-supported clock frequencies. + // Since there are multiple spots in the graphics driver that heavily rely on the above assumption, + // `AppleIntelFramebufferController::probeCDClockFrequency()` is wrapped to adjust the frequency if necessary. + // + // If the current Core Display Clock frequency is not natively supported by the driver, + // this patch will reprogram the clock to set its frequency to a supported value that is appropriate for the hardware. + // As such, the kernel panic can be avoided, and the built-in display can be lit up successfully. + // + // If you are interested in the story behind this fix, please take a look at my blog post. + // https://www.firewolf.science/2020/08/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/ + // + // - FireWolf + // - 2020.08 + // + + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Called with controller at 0x%llx.", that); + + // Read the Core Display Clock frequency from the CDCLK_CTL register + // Bit 0 - 11 stores the decimal frequency + auto cdclk = callbackIGFX->modCoreDisplayClockFix.orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; + SYSLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The current core display clock frequency is %s MHz.", + coreDisplayClockDecimalFrequency2String(cdclk)); + + // Guard: Check whether the current frequency is supported by the graphics driver + if (cdclk < ICL_CDCLK_DEC_FREQ_THRESHOLD) { + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is not supported."); + callbackIGFX->modCoreDisplayClockFix.sanitizeCDClockFrequency(that); + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The core display clock has been switched to a supported frequency."); + } + + // Invoke the original method to ensure everything works as expected + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Will invoke the original function."); + auto retVal = callbackIGFX->modCoreDisplayClockFix.orgProbeCDClockFrequency(that); + DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x%llx.", retVal); + return retVal; +} From 1bbc678332adfe48222c8c86ed11351651ba3758 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:13:51 -0700 Subject: [PATCH 075/102] CDC: Relocate constant and register definitions. --- WhateverGreen/kern_igfx_clock.cpp | 146 ++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 0c9dd2b3..9a73edef 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -18,6 +18,8 @@ // MARK: - Maximum Link Rate Fix +// MARK: Constant Definitions + /** * The default DPCD address that stores receiver capabilities (16 bytes) */ @@ -89,6 +91,8 @@ struct DPCDCap16 { // 16 bytes uint8_t others[12]; }; +// MARK: Patch Submodule IMP + void IGFX::DPCDMaxLinkRateFix::init() { // We only need to patch the framebuffer driver requiresPatchingGraphics = false; @@ -319,6 +323,148 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { // MARK: - Core Display Clock Fix +// MARK: Constant Definitions + +/** + * Address of the register used to retrieve the current Core Display Clock frequency + */ +static constexpr uint32_t ICL_REG_CDCLK_CTL = 0x46000; + +/** + * Address of the register used to retrieve the hardware reference clock frequency + */ +static constexpr uint32_t ICL_REG_DSSM = 0x51004; + +/** + * Enumerates all possible hardware reference clock frequencies on ICL platforms + * + * Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c + * Command Reference: Registers Part 1 – Registers A through L, DSSM Register + */ +enum ICLReferenceClockFrequency { + + // 24 MHz + ICL_REF_CLOCK_FREQ_24_0 = 0x0, + + // 19.2 MHz + ICL_REF_CLOCK_FREQ_19_2 = 0x1, + + // 38.4 MHz + ICL_REF_CLOCK_FREQ_38_4 = 0x2 +}; + +/** + * Enumerates all possible Core Display Clock decimal frequency + * + * Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c + * Command Reference: Registers Part 1 – Registers A through L, CDCLK_CTL Register + */ +enum ICLCoreDisplayClockDecimalFrequency { + + // 172.8 MHz + ICL_CDCLK_FREQ_172_8 = 0x158, + + // 180 MHz + ICL_CDCLK_FREQ_180_0 = 0x166, + + // 192 MHz + ICL_CDCLK_FREQ_192_0 = 0x17E, + + // 307.2 MHz + ICL_CDCLK_FREQ_307_2 = 0x264, + + // 312 MHz + ICL_CDCLK_FREQ_312_0 = 0x26E, + + // 552 MHz + ICL_CDCLK_FREQ_552_0 = 0x44E, + + // 556.8 MHz + ICL_CDCLK_FREQ_556_8 = 0x458, + + // 648 MHz + ICL_CDCLK_FREQ_648_0 = 0x50E, + + // 652.8 MHz + ICL_CDCLK_FREQ_652_8 = 0x518 +}; + +/** + * Get the string representation of the given Core Display Clock decimal frequency + */ +static inline const char* coreDisplayClockDecimalFrequency2String(uint32_t frequency) { + switch (frequency) { + case ICL_CDCLK_FREQ_172_8: + return "172.8"; + + case ICL_CDCLK_FREQ_180_0: + return "180"; + + case ICL_CDCLK_FREQ_192_0: + return "192"; + + case ICL_CDCLK_FREQ_307_2: + return "307.2"; + + case ICL_CDCLK_FREQ_312_0: + return "312"; + + case ICL_CDCLK_FREQ_552_0: + return "552"; + + case ICL_CDCLK_FREQ_556_8: + return "556.8"; + + case ICL_CDCLK_FREQ_648_0: + return "648"; + + case ICL_CDCLK_FREQ_652_8: + return "652.8"; + + default: + return "INVALID"; + } +} + +/** + * Any Core Display Clock frequency lower than this value is not supported by the driver + * + * @note This threshold is derived from the ICL framebuffer driver on macOS 10.15.6. + */ +static constexpr uint32_t ICL_CDCLK_DEC_FREQ_THRESHOLD = ICL_CDCLK_FREQ_648_0; + +/** + * Core Display Clock PLL frequency in Hz for the 24 MHz hardware reference frequency + * + * Main Reference: + * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 12 Display Engine + * Page 171, CDCLK PLL Ratio and Divider Programming and Resulting Frequencies. + * + * Side Reference: + * - Intel Graphics Driver for Linux (5.8.3) bxt_calc_cdclk_pll_vco() in intel_cdclk.c + * + * @note 54 is the PLL ratio when the reference frequency is 24 MHz + */ +static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_24_0 = 24000000 * 54; + +/** + * Core Display Clock PLL frequency in Hz for the 19.2 MHz hardware reference frequency + * + * @note 68 is the PLL ratio when the reference frequency is 19.2 MHz + */ +static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_19_2 = 19200000 * 68; + +/** + * Core Display Clock PLL frequency in Hz for the 38.4 MHz hardware reference frequency + * + * @note 34 is the PLL ratio when the reference frequency is 19.2 MHz + */ +static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_38_4 = 38400000 * 34; + +// MARK: Patch Submodule IMP + void IGFX::CoreDisplayClockFix::init() { // We only need to patch the framebuffer driver requiresPatchingGraphics = false; From d7afcd987b748162be5637c2f7516501dea5943b Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:20:07 -0700 Subject: [PATCH 076/102] CDC: Remove the old version. --- WhateverGreen/kern_igfx.cpp | 131 +------------------------- WhateverGreen/kern_igfx.hpp | 180 +----------------------------------- 2 files changed, 2 insertions(+), 309 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index eeb071ad..fc48684c 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -282,12 +282,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { // Enable the verbose output in I2C-over-AUX transactions if the corresponding boot argument is found verboseI2C = checkKernelArgument("-igfxi2cdbg"); - // Enable the Core Display Clock patch on ICL platforms - coreDisplayClockPatch = checkKernelArgument("-igfxcdc"); - // Or if `enable-cdclk-frequency-fix` is set in IGPU property - if (!coreDisplayClockPatch) - coreDisplayClockPatch = info->videoBuiltin->getProperty("enable-cdclk-frequency-fix") != nullptr; - // Iterate through each submodule and redirect the request for (auto submodule : submodules) submodule->processKernel(patcher, info); @@ -371,8 +365,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (supportLSPCON) return true; - if (coreDisplayClockPatch) - return true; if (forceCompleteModeset.enable) return true; if (forceOnlineDisplay.enable) @@ -485,7 +477,7 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a bool bklKabyFb = realFramebuffer == &kextIntelKBLFb && cflBacklightPatch == CoffeeBacklightPatch::On; // Solve ReadRegister32 just once as it is shared if (bklCoffeeFb || bklKabyFb || - RPSControl.enabled || ForceWakeWorkaround.enabled || coreDisplayClockPatch) { + RPSControl.enabled || ForceWakeWorkaround.enabled || modCoreDisplayClockFix.enabled) { AppleIntelFramebufferController__ReadRegister32 = patcher.solveSymbol (index, "__ZN31AppleIntelFramebufferController14ReadRegister32Em", address, size); if (!AppleIntelFramebufferController__ReadRegister32) @@ -550,28 +542,6 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a patcher.clearError(); } } - - if (coreDisplayClockPatch) { - auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); - auto dcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); - auto scdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); - if (pcdcAddress && dcdcAddress && scdcAddress && AppleIntelFramebufferController__ReadRegister32) { - patcher.eraseCoverageInstPrefix(pcdcAddress); - orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); - orgDisableCDClock = reinterpret_cast(dcdcAddress); - orgSetCDClockFrequency = reinterpret_cast(scdcAddress); - orgIclReadRegister32 = AppleIntelFramebufferController__ReadRegister32; - if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { - DBGLOG("igfx", "CDC: Functions have been routed successfully."); - } else { - patcher.clearError(); - SYSLOG("igfx", "CDC: Failed to route functions."); - } - } else { - SYSLOG("igfx", "CDC: Failed to find symbols."); - patcher.clearError(); - } - } // Iterate through each submodule and redirect the request if and only if the submodule is enabled for (auto submodule : submodules) @@ -1547,105 +1517,6 @@ IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *d return retVal; } -void IGFX::sanitizeCDClockFrequency(void *that) -{ - // Read the hardware reference frequency from the DSSM register - // Bits 29-31 store the reference frequency value - auto referenceFrequency = callbackIGFX->orgIclReadRegister32(that, ICL_REG_DSSM) >> 29; - - // Frequency of Core Display Clock PLL is determined by the reference frequency - uint32_t newCdclkFrequency = 0; - uint32_t newPLLFrequency = 0; - switch (referenceFrequency) { - case ICL_REF_CLOCK_FREQ_19_2: - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 19.2 MHz."); - newCdclkFrequency = ICL_CDCLK_FREQ_652_8; - newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_19_2; - break; - - case ICL_REF_CLOCK_FREQ_24_0: - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 24.0 MHz."); - newCdclkFrequency = ICL_CDCLK_FREQ_648_0; - newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_24_0; - break; - - case ICL_REF_CLOCK_FREQ_38_4: - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Reference frequency is 38.4 MHz."); - newCdclkFrequency = ICL_CDCLK_FREQ_652_8; - newPLLFrequency = ICL_CDCLK_PLL_FREQ_REF_38_4; - break; - - default: - SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() Error: Reference frequency is invalid. Will panic later."); - return; - } - - // Debug: Print the new frequencies - SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency will be set to %s MHz.", - coreDisplayClockDecimalFrequency2String(newCdclkFrequency)); - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL frequency will be set to %u Hz.", newPLLFrequency); - - // Disable the Core Display Clock PLL - callbackIGFX->orgDisableCDClock(that); - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock PLL has been disabled."); - - // Set the new PLL frequency and reenable the Core Display Clock PLL - callbackIGFX->orgSetCDClockFrequency(that, newPLLFrequency); - DBGLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock has been reprogrammed and PLL has been re-enabled."); - - // "Verify" that the new frequency is effective - auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; - SYSLOG("igfx", "CDC: sanitizeCDClockFrequency() DInfo: Core Display Clock frequency is %s MHz now.", - coreDisplayClockDecimalFrequency2String(cdclk)); -} - -uint32_t IGFX::wrapProbeCDClockFrequency(void *that) { - // - // Abstract - // - // 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 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, such as 172.8 MHz, - // and hence a kernel panic occurs to indicate the precondition failure. - // - // This reverse engineering research analyzes functions related to configuring the Core Display Clock - // and exploits existing APIs to add support for those valid yet non-supported clock frequencies. - // Since there are multiple spots in the graphics driver that heavily rely on the above assumption, - // `AppleIntelFramebufferController::probeCDClockFrequency()` is wrapped to adjust the frequency if necessary. - // - // If the current Core Display Clock frequency is not natively supported by the driver, - // this patch will reprogram the clock to set its frequency to a supported value that is appropriate for the hardware. - // As such, the kernel panic can be avoided, and the built-in display can be lit up successfully. - // - // If you are interested in the story behind this fix, please take a look at my blog post. - // https://www.firewolf.science/2020/08/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/ - // - // - FireWolf - // - 2020.08 - // - - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Called with controller at 0x%llx.", that); - - // Read the Core Display Clock frequency from the CDCLK_CTL register - // Bit 0 - 11 stores the decimal frequency - auto cdclk = callbackIGFX->orgIclReadRegister32(that, ICL_REG_CDCLK_CTL) & 0x7FF; - SYSLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The current core display clock frequency is %s MHz.", - coreDisplayClockDecimalFrequency2String(cdclk)); - - // Guard: Check whether the current frequency is supported by the graphics driver - if (cdclk < ICL_CDCLK_DEC_FREQ_THRESHOLD) { - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The currrent core display clock frequency is not supported."); - callbackIGFX->sanitizeCDClockFrequency(that); - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The core display clock has been switched to a supported frequency."); - } - - // Invoke the original method to ensure everything works as expected - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: Will invoke the original function."); - auto retVal = callbackIGFX->orgProbeCDClockFrequency(that); - DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x%llx.", retVal); - return retVal; -} - void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { if (reg == BXT_BLC_PWM_FREQ1) { if (value && value != callbackIGFX->driverBacklightFrequency) { diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index d3db1b6c..542dcea9 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -296,7 +296,6 @@ class IGFX { */ uint32_t (*orgCflReadRegister32)(void *, uint32_t) {nullptr}; uint32_t (*orgKblReadRegister32)(void *, uint32_t) {nullptr}; - uint32_t (*orgIclReadRegister32)(void *, uint32_t) {nullptr}; /** * Original AppleIntelFramebufferController::WriteRegister32 function @@ -319,28 +318,6 @@ class IGFX { */ IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *) {nullptr}; - /** - * Original AppleIntelFramebufferController::probeCDClockFrequency function (ICL) - */ - uint32_t (*orgProbeCDClockFrequency)(void *) {nullptr}; - - /** - * Original AppleIntelFramebufferController::disableCDClock function (ICL) - */ - void (*orgDisableCDClock)(void *) {nullptr}; - - /** - * Original AppleIntelFramebufferController::setCDClockFrequency function (ICL) - * - * @param frequency The Core Display Clock PLL frequency in Hz - */ - void (*orgSetCDClockFrequency)(void *, unsigned long long) {nullptr}; - - /** - * Patch the Core Display Clock frequency if ncessary - */ - bool coreDisplayClockPatch {false}; - /** * Set to true if a black screen ComputeLaneCount patch is required */ @@ -811,7 +788,7 @@ class IGFX { /** * A collection of submodules */ - PatchSubmodule *submodules[2] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix }; + PatchSubmodule *submodules[3] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix, &modCoreDisplayClockFix }; /** * Ensure each modeset is a complete modeset. @@ -918,146 +895,6 @@ class IGFX { * Driver-requested backlight frequency obtained from BXT_BLC_PWM_FREQ1 write attempt at system start. */ uint32_t driverBacklightFrequency {}; - - /** - * Address of the register used to retrieve the current Core Display Clock frequency - */ - static constexpr uint32_t ICL_REG_CDCLK_CTL = 0x46000; - - /** - * Address of the register used to retrieve the hardware reference clock frequency - */ - static constexpr uint32_t ICL_REG_DSSM = 0x51004; - - /** - * Enumerates all possible hardware reference clock frequencies on ICL platforms - * - * Reference: - * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c - * Command Reference: Registers Part 1 – Registers A through L, DSSM Register - */ - enum ICLReferenceClockFrequency - { - // 24 MHz - ICL_REF_CLOCK_FREQ_24_0 = 0x0, - - // 19.2 MHz - ICL_REF_CLOCK_FREQ_19_2 = 0x1, - - // 38.4 MHz - ICL_REF_CLOCK_FREQ_38_4 = 0x2 - }; - - /** - * Enumerates all possible Core Display Clock decimal frequency - * - * Reference: - * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 2c - * Command Reference: Registers Part 1 – Registers A through L, CDCLK_CTL Register - */ - enum ICLCoreDisplayClockDecimalFrequency - { - // 172.8 MHz - ICL_CDCLK_FREQ_172_8 = 0x158, - - // 180 MHz - ICL_CDCLK_FREQ_180_0 = 0x166, - - // 192 MHz - ICL_CDCLK_FREQ_192_0 = 0x17E, - - // 307.2 MHz - ICL_CDCLK_FREQ_307_2 = 0x264, - - // 312 MHz - ICL_CDCLK_FREQ_312_0 = 0x26E, - - // 552 MHz - ICL_CDCLK_FREQ_552_0 = 0x44E, - - // 556.8 MHz - ICL_CDCLK_FREQ_556_8 = 0x458, - - // 648 MHz - ICL_CDCLK_FREQ_648_0 = 0x50E, - - // 652.8 MHz - ICL_CDCLK_FREQ_652_8 = 0x518 - }; - - /** - * Get the string representation of the given Core Display Clock decimal frequency - */ - static inline const char* coreDisplayClockDecimalFrequency2String(uint32_t frequency) - { - switch (frequency) - { - case ICL_CDCLK_FREQ_172_8: - return "172.8"; - - case ICL_CDCLK_FREQ_180_0: - return "180"; - - case ICL_CDCLK_FREQ_192_0: - return "192"; - - case ICL_CDCLK_FREQ_307_2: - return "307.2"; - - case ICL_CDCLK_FREQ_312_0: - return "312"; - - case ICL_CDCLK_FREQ_552_0: - return "552"; - - case ICL_CDCLK_FREQ_556_8: - return "556.8"; - - case ICL_CDCLK_FREQ_648_0: - return "648"; - - case ICL_CDCLK_FREQ_652_8: - return "652.8"; - - default: - return "INVALID"; - } - } - - /** - * Any Core Display Clock frequency lower than this value is not supported by the driver - * - * @note This threshold is derived from the ICL framebuffer driver on macOS 10.15.6. - */ - static constexpr uint32_t ICL_CDCLK_DEC_FREQ_THRESHOLD = ICL_CDCLK_FREQ_648_0; - - /** - * Core Display Clock PLL frequency in Hz for the 24 MHz hardware reference frequency - * - * Main Reference: - * - Intel Graphics Developer Manaual for Ice Lake Platforms, Volume 12 Display Engine - * Page 171, CDCLK PLL Ratio and Divider Programming and Resulting Frequencies. - * - * Side Reference: - * - Intel Graphics Driver for Linux (5.8.3) bxt_calc_cdclk_pll_vco() in intel_cdclk.c - * - * @note 54 is the PLL ratio when the reference frequency is 24 MHz - */ - static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_24_0 = 24000000 * 54; - - /** - * Core Display Clock PLL frequency in Hz for the 19.2 MHz hardware reference frequency - * - * @note 68 is the PLL ratio when the reference frequency is 19.2 MHz - */ - static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_19_2 = 19200000 * 68; - - /** - * Core Display Clock PLL frequency in Hz for the 38.4 MHz hardware reference frequency - * - * @note 34 is the PLL ratio when the reference frequency is 19.2 MHz - */ - static constexpr uint32_t ICL_CDCLK_PLL_FREQ_REF_38_4 = 38400000 * 34; /** * See function definition for explanation @@ -1777,21 +1614,6 @@ class IGFX { */ static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); - /** - * [Helper] A helper to change the Core Display Clock frequency to a supported value - */ - static void sanitizeCDClockFrequency(void *that); - - /** - * [Wrapper] Probe and adjust the Core Display Clock frequency if necessary - * - * @param that The hidden implicit `this` pointer - * @return The PLL VCO frequency in Hz derived from the current Core Display Clock frequency. - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::probeCDClockFrequency()` method. - * Used to inject code to reprogram the clock so that its frequency is natively supported by the driver. - */ - static uint32_t wrapProbeCDClockFrequency(void *that); - /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates */ From 923340931268b424b2f333544e972cdcf6aff6a4 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:36:10 -0700 Subject: [PATCH 077/102] HDC: Add the patch submodule definition for the HDMI Dividers Calculation fix. --- WhateverGreen/kern_igfx.hpp | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 542dcea9..38f9d228 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -784,6 +784,70 @@ class IGFX { void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modCoreDisplayClockFix; + + /** + * A submodule to fix the calculation of HDMI dividers to avoid the infinite loop + */ + class HDMIDividersCalcFix: public PatchSubmodule + { + private: + /** + * Represents the current context of probing dividers for HDMI connections + */ + struct ProbeContext { + /// The current minimum deviation + uint64_t minDeviation; + + /// The current chosen central frequency + uint64_t central; + + /// The current DCO frequency + uint64_t frequency; + + /// The current selected divider + uint32_t divider; + + /// The corresponding pdiv value [P0] + uint32_t pdiv; + + /// The corresponding qdiv value [P1] + uint32_t qdiv; + + /// The corresponding kqiv value [P2] + uint32_t kdiv; + }; + + /** + * [Helper] Compute the final P0, P1, P2 values based on the current frequency divider + * + * @param context The current context for probing P0, P1 and P2. + * @note Implementation adopted from the Intel Graphics Programmer Reference Manual; + * Volume 12 Display, Page 135, Algorithm to Find HDMI and DVI DPLL Programming. + * Volume 12 Display, Page 135, Pseudo-code for HDMI and DVI DPLL Programming. + * @ref static void skl_wrpll_get_multipliers(p:p0:p1:p2:) + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 + */ + static void populateP0P1P2(struct ProbeContext* context); + + /** + * Compute dividers for a HDMI connection with the given pixel clock + * + * @param that The hidden implicit `this` pointer + * @param pixelClock The pixel clock value (in Hz) used for the HDMI connection + * @param displayPath The corresponding display path + * @param parameters CRTC parameters populated on return + * @return Never used by its caller, so this method might return void. + * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` + */ + static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); + + public: + // MARK: Patch Submodule IMP + void init() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modHDMIDividersCalcFix; /** * A collection of submodules From 072a0f7e5dbd16a2b3cf4f8f41fefa1c826af562 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:53:00 -0700 Subject: [PATCH 078/102] HDC: Relocate the HDMI Dividers Calculation fix as a patch submodule. --- WhateverGreen/kern_igfx.hpp | 2 +- WhateverGreen/kern_igfx_clock.cpp | 221 ++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 38f9d228..0ba4f5e0 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -840,7 +840,7 @@ class IGFX { * @return Never used by its caller, so this method might return void. * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` */ - static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); + static void wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); public: // MARK: Patch Submodule IMP diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 9a73edef..c5b5d8b5 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -14,6 +14,7 @@ /// /// 1. Maximum Link Rate fix for eDP panel on CFL+. /// 2. Core Display Clock fix for the graphics engine on ICL+. +/// 3. HDMI Dividers Calculation fix on SKL, KBL, CFL. /// // MARK: - Maximum Link Rate Fix @@ -599,3 +600,223 @@ uint32_t IGFX::CoreDisplayClockFix::wrapProbeCDClockFrequency(void *that) { DBGLOG("igfx", "CDC: ProbeCDClockFrequency() DInfo: The original function returns 0x%llx.", retVal); return retVal; } + +// MARK: - HDMI Dividers Calculation Fix + +// MARK: Constant Definitions + +// MARK: Patch Submodule IMP + +void IGFX::HDMIDividersCalcFix::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::HDMIDividersCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable the fix for computing HDMI dividers on SKL, KBL, CFL platforms if the corresponding boot argument is found + enabled = checkKernelArgument("-igfxhdmidivs"); + // Of if "enable-hdmi-dividers-fix" is set in IGPU property + if (!enabled) + enabled = info->videoBuiltin->getProperty("enable-hdmi-dividers-fix") != nullptr; +} + +void IGFX::HDMIDividersCalcFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController17ComputeHdmiP0P1P2EjP21AppleIntelDisplayPathPNS_10CRTCParamsE", wrapComputeHdmiP0P1P2); + if (!patcher.routeMultiple(index, &request, 1, address, size)) + SYSLOG("igfx", "HDC: Failed to route the function."); +} + +void IGFX::HDMIDividersCalcFix::populateP0P1P2(struct ProbeContext *context) { + uint32_t p = context->divider; + uint32_t p0 = 0; + uint32_t p1 = 0; + uint32_t p2 = 0; + + // Even divider + if (p % 2 == 0) { + uint32_t half = p / 2; + if (half == 1 || half == 2 || half == 3 || half == 5) { + p0 = 2; + p1 = 1; + p2 = half; + } else if (half % 2 == 0) { + p0 = 2; + p1 = half / 2; + p2 = 2; + } else if (half % 3 == 0) { + p0 = 3; + p1 = half / 3; + p2 = 2; + } else if (half % 7 == 0) { + p0 = 7; + p1 = half / 7; + p2 = 2; + } + } + // Odd divider + else if (p == 3 || p == 9) { + p0 = 3; + p1 = 1; + p2 = p / 3; + } else if (p == 5 || p == 7) { + p0 = p; + p1 = 1; + p2 = 1; + } else if (p == 15) { + p0 = 3; + p1 = 1; + p2 = 5; + } else if (p == 21) { + p0 = 7; + p1 = 1; + p2 = 3; + } else if (p == 35) { + p0 = 7; + p1 = 1; + p2 = 5; + } + + context->pdiv = p0; + context->qdiv = p1; + context->kdiv = p2; +} + +void IGFX::HDMIDividersCalcFix::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) { + // + // Abstract + // + // ComputeHdmiP0P1P2 is used to compute required parameters to establish the HDMI connection. + // Apple's original implementation cannot find appropriate dividers for a higher pixel clock + // value like 533.25 MHz (HDMI 2.0 4K @ 60Hz) and then causes an infinite loop in the kernel. + // + // This reverse engineering research focuses on identifying fields in CRTC parameters by + // carefully analyzing Apple's original implementation, and a better implementation that + // conforms to Intel Graphics Programmer Reference Manual is provided to fix the issue. + // + // This is the first stage to try to solve the HDMI 2.0 output issue on Dell XPS 15 9570, + // and is now succeeded by the LSPCON driver solution. + // LSPCON is used to convert DisplayPort signal to HDMI 2.0 signal. + // When the onboard LSPCON chip is running in LS mode, macOS recognizes the HDMI port as + // a HDMI port. Consequently, ComputeHdmiP0P1P2() is called by SetupClock() to compute + // the parameters for the connection. + // In comparison, when the chip is running in PCON mode, macOS recognizes the HDMI port as + // a DisplayPort port. As a result, this method is never called by SetupClock(). + // + // This fix is left here as an emergency fallback and for reference purposes, + // and is compatible for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + // Note that it is still capable of finding appropriate dividers for a 1080p HDMI connection + // and is more robust than the original implementation. + // + // For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their + // HDMI 1.4 port, you might find this fix helpful. + // + // For those who have a laptop or PC with HDMI 2.0 routed to IGPU and have HDMI output issues, + // it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. + // + // - FireWolf + // - 2019.06 + // + + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: Called with pixel clock = %d Hz.", pixelClock); + + /// All possible dividers + static constexpr uint32_t dividers[] = { + // Even dividers + 4, 6, 8, 10, 12, 14, 16, 18, 20, + 24, 28, 30, 32, 36, 40, 42, 44, 48, + 52, 54, 56, 60, 64, 66, 68, 70, 72, + 76, 78, 80, 84, 88, 90, 92, 96, 98, + + // Odd dividers + 3, 5, 7, 9, 15, 21, 35 + }; + + /// All possible central frequency values + static constexpr uint64_t centralFrequencies[3] = {8400000000ULL, 9000000000ULL, 9600000000ULL}; + + // Calculate the AFE clock + uint64_t afeClock = static_cast(pixelClock) * 5; + + // Prepare the context for probing P0, P1 and P2 + ProbeContext context {}; + + // Apple chooses 400 as the initial minimum deviation + // However 400 is too small for a pixel clock like 533.25 MHz (HDMI 2.0 4K @ 60Hz) + // Raise the value to UInt64 MAX + // It's OK because the deviation is still bound by MAX_POS_DEV and MAX_NEG_DEV. + context.minDeviation = UINT64_MAX; + + for (auto divider : dividers) { + for (auto central : centralFrequencies) { + // Calculate the current DCO frequency + uint64_t frequency = divider * afeClock; + // Calculate the deviation + uint64_t deviation = (frequency > central ? frequency - central : central - frequency) * 10000 / central; + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: Dev = %6llu; Central = %10llu Hz; DCO Freq = %12llu Hz; Divider = %2d.\n", deviation, central, frequency, divider); + + // Guard: Positive deviation is within the allowed range + if (frequency >= central && deviation >= SKL_DCO_MAX_POS_DEVIATION) + continue; + + // Guard: Negative deviation is within the allowed range + if (frequency < central && deviation >= SKL_DCO_MAX_NEG_DEVIATION) + continue; + + // Guard: Less than the current minimum deviation value + if (deviation >= context.minDeviation) + continue; + + // Found a better one + // Update the value + context.minDeviation = deviation; + context.central = central; + context.frequency = frequency; + context.divider = divider; + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: FOUND: Min Dev = %8llu; Central = %10llu Hz; Freq = %12llu Hz; Divider = %d\n", deviation, central, frequency, divider); + + // Guard: Check whether the new minmimum deviation has been reduced to 0 + if (deviation != 0) + continue; + + // Guard: An even divider is preferred + if (divider % 2 == 0) { + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: Found an even divider [%d] with deviation 0.\n", divider); + break; + } + } + } + + // Guard: A valid divider has been found + if (context.divider == 0) { + SYSLOG("igfx", "HDC: ComputeHdmiP0P1P2() Error: Cannot find a valid divider for the given pixel clock %d Hz.\n", pixelClock); + return; + } + + // Calculate the p,q,k dividers + populateP0P1P2(&context); + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: Divider = %d --> P0 = %d; P1 = %d; P2 = %d.\n", context.divider, context.pdiv, context.qdiv, context.kdiv); + + // Calculate the CRTC parameters + uint32_t multiplier = (uint32_t) (context.frequency / 24000000); + uint32_t fraction = (uint32_t) (context.frequency - multiplier * 24000000); + uint32_t cf15625 = (uint32_t) (context.central / 15625); + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: Multiplier = %d; Fraction = %d; CF15625 = %d.\n", multiplier, fraction, cf15625); + + // Guard: The given CRTC parameters should never be NULL + if (parameters == nullptr) { + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() Error: The given CRTC parameters should not be NULL."); + return; + } + + // Save all parameters + auto params = reinterpret_cast(parameters); + params->pdiv = context.pdiv; + params->qdiv = context.qdiv; + params->kdiv = context.kdiv; + params->multiplier = multiplier; + params->fraction = fraction; + params->cf15625 = cf15625; + DBGLOG("igfx", "HDC: ComputeHdmiP0P1P2() DInfo: CTRC parameters have been populated successfully."); + return; +} From 403ec7072b99922f70545bb18c0b7b8e5efbc770 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 20 Sep 2020 23:59:43 -0700 Subject: [PATCH 079/102] HDC: Relocate constant and structure definitions. --- WhateverGreen/kern_igfx.hpp | 6 ++-- WhateverGreen/kern_igfx_clock.cpp | 59 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 0ba4f5e0..6ea3816a 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -788,8 +788,7 @@ class IGFX { /** * A submodule to fix the calculation of HDMI dividers to avoid the infinite loop */ - class HDMIDividersCalcFix: public PatchSubmodule - { + class HDMIDividersCalcFix: public PatchSubmodule { private: /** * Represents the current context of probing dividers for HDMI connections @@ -852,7 +851,7 @@ class IGFX { /** * A collection of submodules */ - PatchSubmodule *submodules[3] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix, &modCoreDisplayClockFix }; + PatchSubmodule *submodules[4] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix, &modCoreDisplayClockFix, &modHDMIDividersCalcFix }; /** * Ensure each modeset is a complete modeset. @@ -965,6 +964,7 @@ class IGFX { */ static bool wrapHwRegsNeedUpdate(void *controller, IOService *framebuffer, void *displayPath, void *crtParams, void *detailedInfo); + // TODO: RELOCATED /** * Reflect the `AppleIntelFramebufferController::CRTCParams` struct * diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index c5b5d8b5..56537e0a 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -605,6 +605,65 @@ uint32_t IGFX::CoreDisplayClockFix::wrapProbeCDClockFrequency(void *that) { // MARK: Constant Definitions +/** + * The maximum positive deviation from the DCO central frequency + * + * @note DCO frequency must be within +1% of the DCO central frequency. + * @warning This is a hardware requirement. + * See "Intel Graphics Programmer Reference Manual for Kaby Lake platform" + * Volume 12 Display, Page 134, Formula for HDMI and DVI DPLL Programming + * @link https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf + * @note This value is appropriate for graphics on Skylake, Kaby Lake and Coffee Lake platforms. + * @seealso Intel Linux Graphics Driver + * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1080 + */ +static constexpr uint64_t SKL_DCO_MAX_POS_DEVIATION = 100; + +/** + * The maximum negative deviation from the DCO central frequency + * + * @note DCO frequency must be within -6% of the DCO central frequency. + * @seealso See `SKL_DCO_MAX_POS_DEVIATION` above for details. + */ +static constexpr uint64_t SKL_DCO_MAX_NEG_DEVIATION = 600; + +/** + * Reflect the `AppleIntelFramebufferController::CRTCParams` struct + * + * @note Unlike the Intel Linux Graphics Driver, + * - Apple does not transform the `pdiv`, `qdiv` and `kdiv` fields. + * - Apple records the final central frequency divided by 15625. + * @ref static void skl_wrpll_params_populate(params:afe_clock:central_freq:p0:p1:p2:) + * @seealso https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1171 + */ +struct CRTCParams { + /// Uninvestigated fields + uint8_t uninvestigated[32]; + + /// P0 [`CRTCParams` field offset 0x20] + uint32_t pdiv; + + /// P1 [`CRTCParams` field offset 0x24] + uint32_t qdiv; + + /// P2 [`CRTCParams` field offset 0x28] + uint32_t kdiv; + + /// Difference in Hz [`CRTCParams` field offset 0x2C] + uint32_t fraction; + + /// Multiplier of 24 MHz [`CRTCParams` field offset 0x30] + uint32_t multiplier; + + /// Central Frequency / 15625 [`CRTCParams` field offset 0x34] + uint32_t cf15625; + + /// The rest fields are not of interest +}; + +static_assert(offsetof(CRTCParams, pdiv) == 0x20, "Invalid pdiv offset, please check your compiler."); +static_assert(sizeof(CRTCParams) == 56, "Invalid size of CRTCParams struct, please check your compiler."); + // MARK: Patch Submodule IMP void IGFX::HDMIDividersCalcFix::init() { From 3563baa3869b360c556f61b0454dda4f77eb84a5 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 00:07:38 -0700 Subject: [PATCH 080/102] HDC: Remove the old version. --- WhateverGreen/kern_igfx.cpp | 205 ------------------------------------ WhateverGreen/kern_igfx.hpp | 116 -------------------- 2 files changed, 321 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index fc48684c..6616cd88 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -252,12 +252,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { fwLoadMode = FW_APPLE; /* Do nothing, GuC is either unsupported due to low OS or Apple */ } - // Enable the fix for computing HDMI dividers on SKL, KBL, CFL platforms if the corresponding boot argument is found - hdmiP0P1P2Patch = checkKernelArgument("-igfxhdmidivs"); - // Of if "enable-hdmi-dividers-fix" is set in IGPU property - if (!hdmiP0P1P2Patch) - hdmiP0P1P2Patch = info->videoBuiltin->getProperty("enable-hdmi-dividers-fix") != nullptr; - // Enable the LSPCON driver support if the corresponding boot argument is found supportLSPCON = checkKernelArgument("-igfxlspcon"); // Or if "enable-lspcon-support" is set in IGPU property @@ -361,8 +355,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (cflBacklightPatch != CoffeeBacklightPatch::Off) return true; - if (hdmiP0P1P2Patch) - return true; if (supportLSPCON) return true; if (forceCompleteModeset.enable) @@ -581,12 +573,6 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (debugFramebuffer) loadFramebufferDebug(patcher, index, address, size); - if (hdmiP0P1P2Patch) { - KernelPatcher::RouteRequest request("__ZN31AppleIntelFramebufferController17ComputeHdmiP0P1P2EjP21AppleIntelDisplayPathPNS_10CRTCParamsE", wrapComputeHdmiP0P1P2); - if (!patcher.routeMultiple(index, &request, 1, address, size)) - SYSLOG("igfx", "failed to route ComputeHdmiP0P1P2"); - } - if (supportLSPCON) { auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); @@ -952,197 +938,6 @@ uint32_t IGFX::wrapGetDisplayStatus(IOService *framebuffer, void *displayPath) { return ret; } -void IGFX::populateP0P1P2(struct ProbeContext *context) { - uint32_t p = context->divider; - uint32_t p0 = 0; - uint32_t p1 = 0; - uint32_t p2 = 0; - - // Even divider - if (p % 2 == 0) { - uint32_t half = p / 2; - if (half == 1 || half == 2 || half == 3 || half == 5) { - p0 = 2; - p1 = 1; - p2 = half; - } else if (half % 2 == 0) { - p0 = 2; - p1 = half / 2; - p2 = 2; - } else if (half % 3 == 0) { - p0 = 3; - p1 = half / 3; - p2 = 2; - } else if (half % 7 == 0) { - p0 = 7; - p1 = half / 7; - p2 = 2; - } - } - // Odd divider - else if (p == 3 || p == 9) { - p0 = 3; - p1 = 1; - p2 = p / 3; - } else if (p == 5 || p == 7) { - p0 = p; - p1 = 1; - p2 = 1; - } else if (p == 15) { - p0 = 3; - p1 = 1; - p2 = 5; - } else if (p == 21) { - p0 = 7; - p1 = 1; - p2 = 3; - } else if (p == 35) { - p0 = 7; - p1 = 1; - p2 = 5; - } - - context->pdiv = p0; - context->qdiv = p1; - context->kdiv = p2; -} - -int IGFX::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) { - // - // Abstract - // - // ComputeHdmiP0P1P2 is used to compute required parameters to establish the HDMI connection. - // Apple's original implementation cannot find appropriate dividers for a higher pixel clock - // value like 533.25 MHz (HDMI 2.0 4K @ 60Hz) and then causes an infinite loop in the kernel. - // - // This reverse engineering research focuses on identifying fields in CRTC parameters by - // carefully analyzing Apple's original implementation, and a better implementation that - // conforms to Intel Graphics Programmer Reference Manual is provided to fix the issue. - // - // This is the first stage to try to solve the HDMI 2.0 output issue on Dell XPS 15 9570, - // and is now succeeded by the LSPCON driver solution. - // LSPCON is used to convert DisplayPort signal to HDMI 2.0 signal. - // When the onboard LSPCON chip is running in LS mode, macOS recognizes the HDMI port as - // a HDMI port. Consequently, ComputeHdmiP0P1P2() is called by SetupClock() to compute - // the parameters for the connection. - // In comparison, when the chip is running in PCON mode, macOS recognizes the HDMI port as - // a DisplayPort port. As a result, this method is never called by SetupClock(). - // - // This fix is left here as an emergency fallback and for reference purposes, - // and is compatible for graphics on Skylake, Kaby Lake and Coffee Lake platforms. - // Note that it is still capable of finding appropriate dividers for a 1080p HDMI connection - // and is more robust than the original implementation. - // - // For those who want to have "limited" 2K/4K experience (i.e. 2K@59Hz or 4K@30Hz) with their - // HDMI 1.4 port, you might find this fix helpful. - // - // For those who have a laptop or PC with HDMI 2.0 routed to IGPU and have HDMI output issues, - // it is still recommended to enable the LSPCON driver support to have full HDMI 2.0 experience. - // - // - FireWolf - // - 2019.06 - // - - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Called with pixel clock = %d Hz.", pixelClock); - - /// All possible dividers - static constexpr uint32_t dividers[] = { - // Even dividers - 4, 6, 8, 10, 12, 14, 16, 18, 20, - 24, 28, 30, 32, 36, 40, 42, 44, 48, - 52, 54, 56, 60, 64, 66, 68, 70, 72, - 76, 78, 80, 84, 88, 90, 92, 96, 98, - - // Odd dividers - 3, 5, 7, 9, 15, 21, 35 - }; - - /// All possible central frequency values - static constexpr uint64_t centralFrequencies[3] = {8400000000ULL, 9000000000ULL, 9600000000ULL}; - - // Calculate the AFE clock - uint64_t afeClock = static_cast(pixelClock) * 5; - - // Prepare the context for probing P0, P1 and P2 - ProbeContext context {}; - - // Apple chooses 400 as the initial minimum deviation - // However 400 is too small for a pixel clock like 533.25 MHz (HDMI 2.0 4K @ 60Hz) - // Raise the value to UInt64 MAX - // It's OK because the deviation is still bound by MAX_POS_DEV and MAX_NEG_DEV. - context.minDeviation = UINT64_MAX; - - for (auto divider : dividers) { - for (auto central : centralFrequencies) { - // Calculate the current DCO frequency - uint64_t frequency = divider * afeClock; - // Calculate the deviation - uint64_t deviation = (frequency > central ? frequency - central : central - frequency) * 10000 / central; - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Dev = %6llu; Central = %10llu Hz; DCO Freq = %12llu Hz; Divider = %2d.\n", deviation, central, frequency, divider); - - // Guard: Positive deviation is within the allowed range - if (frequency >= central && deviation >= SKL_DCO_MAX_POS_DEVIATION) - continue; - - // Guard: Negative deviation is within the allowed range - if (frequency < central && deviation >= SKL_DCO_MAX_NEG_DEVIATION) - continue; - - // Guard: Less than the current minimum deviation value - if (deviation >= context.minDeviation) - continue; - - // Found a better one - // Update the value - context.minDeviation = deviation; - context.central = central; - context.frequency = frequency; - context.divider = divider; - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: FOUND: Min Dev = %8llu; Central = %10llu Hz; Freq = %12llu Hz; Divider = %d\n", deviation, central, frequency, divider); - - // Guard: Check whether the new minmimum deviation has been reduced to 0 - if (deviation != 0) - continue; - - // Guard: An even divider is preferred - if (divider % 2 == 0) { - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Found an even divider [%d] with deviation 0.\n", divider); - break; - } - } - } - - // Guard: A valid divider has been found - if (context.divider == 0) { - SYSLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: Cannot find a valid divider for the given pixel clock %d Hz.\n", pixelClock); - return 0; - } - - // Calculate the p,q,k dividers - populateP0P1P2(&context); - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Divider = %d --> P0 = %d; P1 = %d; P2 = %d.\n", context.divider, context.pdiv, context.qdiv, context.kdiv); - - // Calculate the CRTC parameters - uint32_t multiplier = (uint32_t) (context.frequency / 24000000); - uint32_t fraction = (uint32_t) (context.frequency - multiplier * 24000000); - uint32_t cf15625 = (uint32_t) (context.central / 15625); - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: Multiplier = %d; Fraction = %d; CF15625 = %d.\n", multiplier, fraction, cf15625); - // Guard: The given CRTC parameters should never be NULL - if (parameters == nullptr) { - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() Error: The given CRTC parameters should not be NULL."); - return 0; - } - auto params = reinterpret_cast(parameters); - params->pdiv = context.pdiv; - params->qdiv = context.qdiv; - params->kdiv = context.kdiv; - params->multiplier = multiplier; - params->fraction = fraction; - params->cf15625 = cf15625; - DBGLOG("igfx", "SC: ComputeHdmiP0P1P2() DInfo: CTRC parameters have been populated successfully."); - return 0; -} - IOReturn IGFX::LSPCON::probe() { // Read the adapter info uint8_t buffer[128] {}; diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 6ea3816a..fdfd6da4 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -350,11 +350,6 @@ class IGFX { */ bool verboseI2C {false}; - /** - * Set to true to fix the infinite loop issue when computing dividers for HDMI connections - */ - bool hdmiP0P1P2Patch {false}; - /** * Set to true if PAVP code should be disabled */ @@ -964,117 +959,6 @@ class IGFX { */ static bool wrapHwRegsNeedUpdate(void *controller, IOService *framebuffer, void *displayPath, void *crtParams, void *detailedInfo); - // TODO: RELOCATED - /** - * Reflect the `AppleIntelFramebufferController::CRTCParams` struct - * - * @note Unlike the Intel Linux Graphics Driver, - * - Apple does not transform the `pdiv`, `qdiv` and `kdiv` fields. - * - Apple records the final central frequency divided by 15625. - * @ref static void skl_wrpll_params_populate(params:afe_clock:central_freq:p0:p1:p2:) - * @seealso https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1171 - */ - struct CRTCParams { - /// Uninvestigated fields - uint8_t uninvestigated[32]; - - /// P0 [`CRTCParams` field offset 0x20] - uint32_t pdiv; - - /// P1 [`CRTCParams` field offset 0x24] - uint32_t qdiv; - - /// P2 [`CRTCParams` field offset 0x28] - uint32_t kdiv; - - /// Difference in Hz [`CRTCParams` field offset 0x2C] - uint32_t fraction; - - /// Multiplier of 24 MHz [`CRTCParams` field offset 0x30] - uint32_t multiplier; - - /// Central Frequency / 15625 [`CRTCParams` field offset 0x34] - uint32_t cf15625; - - /// The rest fields are not of interest - }; - - static_assert(offsetof(CRTCParams, pdiv) == 0x20, "Invalid pdiv offset, please check your compiler."); - static_assert(sizeof(CRTCParams) == 56, "Invalid size of CRTCParams struct, please check your compiler."); - - /** - * Represents the current context of probing dividers for HDMI connections - */ - struct ProbeContext { - /// The current minimum deviation - uint64_t minDeviation; - - /// The current chosen central frequency - uint64_t central; - - /// The current DCO frequency - uint64_t frequency; - - /// The current selected divider - uint32_t divider; - - /// The corresponding pdiv value [P0] - uint32_t pdiv; - - /// The corresponding qdiv value [P1] - uint32_t qdiv; - - /// The corresponding kqiv value [P2] - uint32_t kdiv; - }; - - /** - * The maximum positive deviation from the DCO central frequency - * - * @note DCO frequency must be within +1% of the DCO central frequency. - * @warning This is a hardware requirement. - * See "Intel Graphics Programmer Reference Manual for Kaby Lake platform" - * Volume 12 Display, Page 134, Formula for HDMI and DVI DPLL Programming - * @link https://01.org/sites/default/files/documentation/intel-gfx-prm-osrc-kbl-vol12-display.pdf - * @note This value is appropriate for graphics on Skylake, Kaby Lake and Coffee Lake platforms. - * @seealso Intel Linux Graphics Driver - * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1080 - */ - static constexpr uint64_t SKL_DCO_MAX_POS_DEVIATION = 100; - - /** - * The maximum negative deviation from the DCO central frequency - * - * @note DCO frequency must be within -6% of the DCO central frequency. - * @seealso See `SKL_DCO_MAX_POS_DEVIATION` above for details. - */ - static constexpr uint64_t SKL_DCO_MAX_NEG_DEVIATION = 600; - - /** - * [Helper] Compute the final P0, P1, P2 values based on the current frequency divider - * - * @param context The current context for probing P0, P1 and P2. - * @note Implementation adopted from the Intel Graphics Programmer Reference Manual; - * Volume 12 Display, Page 135, Algorithm to Find HDMI and DVI DPLL Programming. - * Volume 12 Display, Page 135, Pseudo-code for HDMI and DVI DPLL Programming. - * @ref static void skl_wrpll_get_multipliers(p:p0:p1:p2:) - * @seealso Intel Linux Graphics Driver - * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 - */ - static void populateP0P1P2(struct ProbeContext* context); - - /** - * Compute dividers for a HDMI connection with the given pixel clock - * - * @param that The hidden implicit `this` pointer - * @param pixelClock The pixel clock value (in Hz) used for the HDMI connection - * @param displayPath The corresponding display path - * @param parameters CRTC parameters populated on return - * @return Never used by its caller, so this method might return void. - * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` - */ - static int wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); - /** * Explore the framebuffer structure in Apple's Intel graphics driver */ From 0ec9f8d16e7e18f12fafd1f32c4f8bc65a72449b Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 00:26:24 -0700 Subject: [PATCH 081/102] CDC, HDC: Avoid using void* to represent the opaque controller instance. --- WhateverGreen/kern_igfx.hpp | 16 ++++++++-------- WhateverGreen/kern_igfx_clock.cpp | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index fdfd6da4..2c742549 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -732,14 +732,14 @@ class IGFX { * @param address Address of the MMIO register * @return The 32-bit integer read from the register. */ - uint32_t (*orgIclReadRegister32)(void *, uint32_t) {nullptr}; + uint32_t (*orgIclReadRegister32)(AppleIntelFramebufferController *, uint32_t) {nullptr}; /** * [ICL+] Original AppleIntelFramebufferController::probeCDClockFrequency function * * @seealso Refer to the document of `wrapProbeCDClockFrequency()` below. */ - uint32_t (*orgProbeCDClockFrequency)(void *) {nullptr}; + uint32_t (*orgProbeCDClockFrequency)(AppleIntelFramebufferController *) {nullptr}; /** * [ICL+] Original AppleIntelFramebufferController::disableCDClock function @@ -747,7 +747,7 @@ class IGFX { * @param that The implicit hidden framebuffer controller instance * @note This function is required to reprogram the Core Display Clock. */ - void (*orgDisableCDClock)(void *) {nullptr}; + void (*orgDisableCDClock)(AppleIntelFramebufferController *) {nullptr}; /** * [ICL+] Original AppleIntelFramebufferController::setCDClockFrequency function @@ -756,12 +756,12 @@ class IGFX { * @param frequency The Core Display Clock PLL frequency in Hz * @note This function changes the frequency of the Core Display Clock and reenables it. */ - void (*orgSetCDClockFrequency)(void *, unsigned long long) {nullptr}; + void (*orgSetCDClockFrequency)(AppleIntelFramebufferController *, unsigned long long) {nullptr}; /** * [Helper] A helper to change the Core Display Clock frequency to a supported value */ - static void sanitizeCDClockFrequency(void *that); + static void sanitizeCDClockFrequency(AppleIntelFramebufferController *that); /** * [Wrapper] Probe and adjust the Core Display Clock frequency if necessary @@ -771,7 +771,7 @@ class IGFX { * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::probeCDClockFrequency()` method. * Used to inject code to reprogram the clock so that its frequency is natively supported by the driver. */ - static uint32_t wrapProbeCDClockFrequency(void *that); + static uint32_t wrapProbeCDClockFrequency(AppleIntelFramebufferController *that); public: // MARK: Patch Submodule IMP @@ -822,7 +822,7 @@ class IGFX { * @seealso Intel Linux Graphics Driver * https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/gpu/drm/i915/intel_dpll_mgr.c?h=v5.1.13#n1112 */ - static void populateP0P1P2(struct ProbeContext* context); + static void populateP0P1P2(struct ProbeContext *context); /** * Compute dividers for a HDMI connection with the given pixel clock @@ -834,7 +834,7 @@ class IGFX { * @return Never used by its caller, so this method might return void. * @note Method Signature: `AppleIntelFramebufferController::ComputeHdmiP0P1P2(pixelClock:displayPath:parameters:)` */ - static void wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters); + static void wrapComputeHdmiP0P1P2(AppleIntelFramebufferController *that, uint32_t pixelClock, void *displayPath, void *parameters); public: // MARK: Patch Submodule IMP diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 56537e0a..9092cf87 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -490,7 +490,7 @@ void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, s orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); orgDisableCDClock = reinterpret_cast(dcdcAddress); orgSetCDClockFrequency = reinterpret_cast(scdcAddress); - orgIclReadRegister32 = callbackIGFX->AppleIntelFramebufferController__ReadRegister32; + orgIclReadRegister32 = reinterpret_cast(callbackIGFX->AppleIntelFramebufferController__ReadRegister32); if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { DBGLOG("igfx", "CDC: Functions have been routed successfully."); } else { @@ -503,7 +503,7 @@ void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, s } } -void IGFX::CoreDisplayClockFix::sanitizeCDClockFrequency(void *that) { +void IGFX::CoreDisplayClockFix::sanitizeCDClockFrequency(AppleIntelFramebufferController *that) { // Read the hardware reference frequency from the DSSM register // Bits 29-31 store the reference frequency value auto referenceFrequency = callbackIGFX->modCoreDisplayClockFix.orgIclReadRegister32(that, ICL_REG_DSSM) >> 29; @@ -554,7 +554,7 @@ void IGFX::CoreDisplayClockFix::sanitizeCDClockFrequency(void *that) { coreDisplayClockDecimalFrequency2String(cdclk)); } -uint32_t IGFX::CoreDisplayClockFix::wrapProbeCDClockFrequency(void *that) { +uint32_t IGFX::CoreDisplayClockFix::wrapProbeCDClockFrequency(AppleIntelFramebufferController *that) { // // Abstract // @@ -741,7 +741,7 @@ void IGFX::HDMIDividersCalcFix::populateP0P1P2(struct ProbeContext *context) { context->kdiv = p2; } -void IGFX::HDMIDividersCalcFix::wrapComputeHdmiP0P1P2(void *that, uint32_t pixelClock, void *displayPath, void *parameters) { +void IGFX::HDMIDividersCalcFix::wrapComputeHdmiP0P1P2(AppleIntelFramebufferController *that, uint32_t pixelClock, void *displayPath, void *parameters) { // // Abstract // From cb47477ab43ba287c75bf0b66e8f37f651186240 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 15:46:12 -0700 Subject: [PATCH 082/102] I2C: Separate advanced I2C-over-AUX transactions APIs into a submodule. --- WhateverGreen/kern_igfx.hpp | 138 ++++++++++++++++++++ WhateverGreen/kern_igfx_i2c_aux.cpp | 187 ++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 WhateverGreen/kern_igfx_i2c_aux.cpp diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 2c742549..339e3cb2 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -843,6 +843,144 @@ class IGFX { void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modHDMIDividersCalcFix; + /** + * A submodule to support advanced I2C-over-AUX transactions on SKL, KBL and CFL platforms + * + * @note LSPCON driver enables this module automatically. + * @note This module is not compatible with ICL platforms yet. + * Not sure if Ice Lake graphics card supports native HDMI 2.0 output. + */ + class AdvancedI2COverAUXSupport: public PatchSubmodule { + private: + /** + * Set to true to enable verbose output in I2C-over-AUX transactions + */ + bool verbose {false}; + + /** + * Original AppleIntelFramebufferController::ReadI2COverAUX function + * + * @seealso Refer to the document of `wrapReadI2COverAUX()` below. + */ + IOReturn (*orgReadI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool, uint8_t) {nullptr}; + + /** + * Original AppleIntelFramebufferController::WriteI2COverAUX function + * + * @seealso Refer to the document of `wrapWriteI2COverAUX()` below. + */ + IOReturn (*orgWriteI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool) {nullptr}; + + public: + /// MARK: I2C-over-AUX Transaction APIs + + /** + * [Basic] Read from an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to read and must be <= 16 (See below) + * @param buffer A buffer to store the read bytes (See below) + * @param intermediate Set `true` if this is an intermediate read (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::ReadI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:flags:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::ReadI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + */ + static IOReturn wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags); + + /** + * [Basic] Write to an I2C adapter via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param length The number of bytes requested to write and must be <= 16 (See below) + * @param buffer A buffer that stores the bytes to write (See below) + * @param intermediate Set `true` if this is an intermediate write (See below) + * @param flags A flag reserved by Apple; currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in + * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. + * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. + * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. + * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) + * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) + * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) + * @note Method Signature: `AppleIntelFramebufferController::WriteI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:)` + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::WriteI2COverAUX()` method. + * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. + */ + static IOReturn wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate); + + /** + * [Advanced] Reposition the offset for an I2C-over-AUX access + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset The address of the next register to access + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advSeekI2COverAUX(framebuffer:displayPath:address:offset:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags); + + /** + * [Advanced] Read from an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to read from + * @param length The number of bytes requested to read starting from `offset` + * @param buffer A non-null buffer to store the bytes + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advReadI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); + + /** + * [Advanced] Write to an I2C slave via the AUX channel + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @param address The 7-bit address of an I2C slave + * @param offset Address of the first register to write to + * @param length The number of bytes requested to write starting from `offset` + * @param buffer A non-null buffer containing the bytes to write + * @param flags A flag reserved by Apple. Currently always 0. + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note Method Signature: `AppleIntelFramebufferController::advWriteI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` + * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. + */ + static IOReturn advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); + + // MARK: Patch Submodule IMP + void init() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modAdvancedI2COverAUXSupport; + + + /** * A collection of submodules */ diff --git a/WhateverGreen/kern_igfx_i2c_aux.cpp b/WhateverGreen/kern_igfx_i2c_aux.cpp new file mode 100644 index 00000000..1d8ebcb8 --- /dev/null +++ b/WhateverGreen/kern_igfx_i2c_aux.cpp @@ -0,0 +1,187 @@ +// +// kern_igfx_i2c_aux.cpp +// WhateverGreen +// +// Created by FireWolf on 9/21/20. +// Copyright © 2020 vit9696. All rights reserved. +// + +#include +#include "kern_igfx.hpp" + +void IGFX::AdvancedI2COverAUXSupport::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::AdvancedI2COverAUXSupport::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable the advanced I2C-over-AUX support if LSPCON is enabled + enabled = callbackIGFX->modLSPCONDriverSupport.enabled; + + // Or if verbose output in transactions is enabled + if (checkKernelArgument("-igfxi2cdbg")) { + enabled = true; + verbose = true; + } +} + +void IGFX::AdvancedI2COverAUXSupport::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); + auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); + + if (roa && woa) { + patcher.eraseCoverageInstPrefix(roa); + patcher.eraseCoverageInstPrefix(woa); + orgReadI2COverAUX = reinterpret_cast(patcher.routeFunction(roa, reinterpret_cast(wrapReadI2COverAUX), true)); + orgWriteI2COverAUX = reinterpret_cast(patcher.routeFunction(woa, reinterpret_cast(wrapWriteI2COverAUX), true)); + if (orgReadI2COverAUX && orgWriteI2COverAUX) { + DBGLOG("igfx", "I2C: Functions have been routed successfully"); + } else { + patcher.clearError(); + SYSLOG("igfx", "I2C: Failed to route functions."); + } + } else { + SYSLOG("igfx", "I2C: Failed to find symbols."); + patcher.clearError(); + } +} + +IOReturn IGFX::AdvancedI2COverAUXSupport::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { + if (callbackIGFX->modAdvancedI2COverAUXSupport.verbose) { + uint32_t index = 0xFF; + AppleIntelFramebufferExplorer::getIndex(framebuffer, index); + DBGLOG("igfx", "I2C: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", + index, address, length, intermediate, flags); + IOReturn retVal = callbackIGFX->modAdvancedI2COverAUXSupport.orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); + DBGLOG("igfx", "I2C: ReadI2COverAUX() returns 0x%x.", retVal); + return retVal; + } else { + return callbackIGFX->modAdvancedI2COverAUXSupport.orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); + } +} + +IOReturn IGFX::AdvancedI2COverAUXSupport::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { + if (callbackIGFX->modAdvancedI2COverAUXSupport.verbose) { + uint32_t index = 0xFF; + AppleIntelFramebufferExplorer::getIndex(framebuffer, index); + DBGLOG("igfx", "I2C: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0.", + index, address, length, intermediate); + IOReturn retVal = callbackIGFX->modAdvancedI2COverAUXSupport.orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); + DBGLOG("igfx", "I2C: WriteI2COverAUX() returns 0x%x.", retVal); + return retVal; + } else { + return callbackIGFX->modAdvancedI2COverAUXSupport.orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); + } +} + +IOReturn IGFX::AdvancedI2COverAUXSupport::advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags) { + // No need to check the given `address` and `offset` + // if they are invalid, the underlying RunAUXCommand() will return an error + // First start the transaction by performing an empty write + IOReturn retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, true); + + // Guard: Check the START transaction + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "I2C: AdvSeekI2COverAUX() Error: Failed to start the I2C transaction. Return value = 0x%x.\n", retVal); + return retVal; + } + + // Write a single byte to the given I2C slave + // and set the Middle-of-Transaction bit to 1 + return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 1, reinterpret_cast(&offset), true); +} + +IOReturn IGFX::AdvancedI2COverAUXSupport::advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { + // Guard: Check the buffer length + if (length == 0) { + SYSLOG("igfx", "I2C: AdvReadI2COverAUX() Error: Buffer length must be non-zero."); + return kIOReturnInvalid; + } + + // Guard: Check the buffer + if (buffer == nullptr) { + SYSLOG("igfx", "I2C: AdvReadI2COverAUX() Error: Buffer cannot be NULL."); + return kIOReturnInvalid; + } + + // Guard: Start the transaction and set the access offset successfully + IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "I2C: AdvReadI2COverAUX() Error: Failed to set the data offset."); + return retVal; + } + + // Process the read request + // ReadI2COverAUX() can only process up to 16 bytes in one AUX transaction + // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header + while (length != 0) { + // Calculate the new length for this I2C-over-AUX transaction + uint16_t newLength = length >= 16 ? 16 : length; + + // This is an intermediate transaction + retVal = wrapReadI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true, flags); + + // Guard: The intermediate transaction succeeded + if (retVal != kIOReturnSuccess) { + // Terminate the transaction + wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); + return retVal; + } + + // Update the buffer position and length + length -= newLength; + buffer += newLength; + } + + // All intermediate transactions succeeded + // Terminate the transaction + return wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); +} + +IOReturn IGFX::AdvancedI2COverAUXSupport::advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { + // Guard: Check the buffer length + if (length == 0) { + SYSLOG("igfx", "I2C: AdvWriteI2COverAUX() Error: Buffer length must be non-zero."); + return kIOReturnInvalid; + } + + // Guard: Check the buffer + if (buffer == nullptr) { + SYSLOG("igfx", "I2C: AdvWriteI2COverAUX() Error: Buffer cannot be NULL."); + return kIOReturnInvalid; + } + + // Guard: Start the transaction and set the access offset successfully + IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "I2C: AdvWriteI2COverAUX() Error: Failed to set the data offset."); + return retVal; + } + + // Process the write request + // WriteI2COverAUX() can only process up to 16 bytes in one AUX transaction + // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header + while (length != 0) { + // Calculate the new length for this I2C-over-AUX transaction + uint16_t newLength = length >= 16 ? 16 : length; + + // This is an intermediate transaction + retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true); + + // Guard: The intermediate transaction succeeded + if (retVal != kIOReturnSuccess) { + // Terminate the transaction + wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); + return retVal; + } + + // Update the buffer position and length + length -= newLength; + buffer += newLength; + } + + // All intermediate transactions succeeded + // Terminate the transaction + return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); +} From 8562a1d1d2340f4d3f7c1450902fad8a25c8f529 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 16:18:06 -0700 Subject: [PATCH 083/102] LSPCON: Separate the LSPCON driver support into a submodule. --- WhateverGreen.xcodeproj/project.pbxproj | 12 + WhateverGreen/kern_igfx_lspcon.cpp | 152 ++++++++ WhateverGreen/kern_igfx_lspcon.hpp | 448 ++++++++++++++++++++++++ 3 files changed, 612 insertions(+) create mode 100644 WhateverGreen/kern_igfx_lspcon.cpp create mode 100644 WhateverGreen/kern_igfx_lspcon.hpp diff --git a/WhateverGreen.xcodeproj/project.pbxproj b/WhateverGreen.xcodeproj/project.pbxproj index 238bb547..00e341fd 100644 --- a/WhateverGreen.xcodeproj/project.pbxproj +++ b/WhateverGreen.xcodeproj/project.pbxproj @@ -35,7 +35,10 @@ CEC0863624331E9B00F5B701 /* kern_agdc.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC0863524331E9B00F5B701 /* kern_agdc.hpp */; }; CEC8E2F020F765E700D3CA3A /* kern_cdf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */; }; CEC8E2F120F765E700D3CA3A /* kern_cdf.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */; }; + D515168325195D58003CF0E6 /* kern_igfx_i2c_aux.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D515168125195D58003CF0E6 /* kern_igfx_i2c_aux.cpp */; }; D5224EF125172B2500D5CF16 /* kern_igfx_clock.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */; }; + D5224F492518928300D5CF16 /* kern_igfx_lspcon.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5224F472518928300D5CF16 /* kern_igfx_lspcon.cpp */; }; + D5224F4A2518928300D5CF16 /* kern_igfx_lspcon.hpp in Headers */ = {isa = PBXBuildFile; fileRef = D5224F482518928300D5CF16 /* kern_igfx_lspcon.hpp */; }; D5C32F5624FC45D30078A824 /* kern_igfx_memory.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */; }; E2BE6CE220FB209400ED2D55 /* kern_fb.hpp in Headers */ = {isa = PBXBuildFile; fileRef = E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */; }; /* End PBXBuildFile section */ @@ -131,7 +134,10 @@ CEC8E2EE20F765E700D3CA3A /* kern_cdf.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_cdf.cpp; sourceTree = ""; }; CEC8E2EF20F765E700D3CA3A /* kern_cdf.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = kern_cdf.hpp; sourceTree = ""; }; CEEF190A239CFDB1005B3BE8 /* FAQ.Chart.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FAQ.Chart.md; sourceTree = ""; }; + D515168125195D58003CF0E6 /* kern_igfx_i2c_aux.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_i2c_aux.cpp; sourceTree = ""; }; D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_clock.cpp; sourceTree = ""; }; + D5224F472518928300D5CF16 /* kern_igfx_lspcon.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_lspcon.cpp; sourceTree = ""; }; + D5224F482518928300D5CF16 /* kern_igfx_lspcon.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = kern_igfx_lspcon.hpp; sourceTree = ""; }; D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = kern_igfx_memory.cpp; sourceTree = ""; }; E2BE6CE120FB209400ED2D55 /* kern_fb.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = kern_fb.hpp; sourceTree = ""; }; /* End PBXFileReference section */ @@ -195,6 +201,9 @@ CE1F61B82432DEE800201DF4 /* kern_igfx_debug.cpp */, D5C32F5524FC45D30078A824 /* kern_igfx_memory.cpp */, D5224EF025172B2500D5CF16 /* kern_igfx_clock.cpp */, + D5224F472518928300D5CF16 /* kern_igfx_lspcon.cpp */, + D5224F482518928300D5CF16 /* kern_igfx_lspcon.hpp */, + D515168125195D58003CF0E6 /* kern_igfx_i2c_aux.cpp */, 2F30012324A00F2800C590C3 /* kern_igfx_pm.cpp */, CE7FC0A820F55E7400138088 /* kern_ngfx.cpp */, CE7FC0A920F55E7400138088 /* kern_ngfx.hpp */, @@ -322,6 +331,7 @@ CE7FC0B520F6809600138088 /* kern_shiki.hpp in Headers */, 1C9CB7B11C789FF500231E41 /* kern_rad.hpp in Headers */, CEC8E2F120F765E700D3CA3A /* kern_cdf.hpp in Headers */, + D5224F4A2518928300D5CF16 /* kern_igfx_lspcon.hpp in Headers */, CE766ED7210763B200A84567 /* kern_guc.hpp in Headers */, CEB402A61F17F5C400716912 /* kern_con.hpp in Headers */, CE19710021C380DF00B02AB4 /* kern_nvhda.hpp in Headers */, @@ -444,6 +454,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D515168325195D58003CF0E6 /* kern_igfx_i2c_aux.cpp in Sources */, CE7FC0AE20F5622700138088 /* kern_igfx.cpp in Sources */, CEC8E2F020F765E700D3CA3A /* kern_cdf.cpp in Sources */, D5224EF125172B2500D5CF16 /* kern_igfx_clock.cpp in Sources */, @@ -458,6 +469,7 @@ D5C32F5624FC45D30078A824 /* kern_igfx_memory.cpp in Sources */, CE405ED91E4A080700AA0B3D /* plugin_start.cpp in Sources */, CE7FC0CB20F682A300138088 /* kern_resources.cpp in Sources */, + D5224F492518928300D5CF16 /* kern_igfx_lspcon.cpp in Sources */, CE1F61B92432DEE800201DF4 /* kern_igfx_debug.cpp in Sources */, CE7FC0AA20F55E7400138088 /* kern_ngfx.cpp in Sources */, 1C748C2D1C21952C0024EED2 /* kern_start.cpp in Sources */, diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp new file mode 100644 index 00000000..0f4beda8 --- /dev/null +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -0,0 +1,152 @@ +// +// kern_igfx_lspcon.cpp +// WhateverGreen +// +// Created by FireWolf on 9/21/20. +// Copyright © 2020 vit9696. All rights reserved. +// + +#include "kern_igfx_lspcon.hpp" +#include "kern_igfx.hpp" + +// MARK: - LSPCON Driver Foundation + +LSPCON *LSPCON::create(void *controller, IORegistryEntry *framebuffer, void *displayPath) { + // Guard: Framebuffer index should exist + uint32_t index; + if (!IGFX::AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) + return nullptr; + + // Call the private constructor + return new LSPCON(controller, framebuffer, displayPath, index); +} + +LSPCON::LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath, uint32_t index) { + this->controller = controller; + this->framebuffer = framebuffer; + this->displayPath = displayPath; + this->index = index; +} + +IOReturn LSPCON::probe() { + // Read the adapter info + uint8_t buffer[128] {}; + IOReturn retVal = IGFX::AdvancedI2COverAUXSupport::advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, 0x00, 128, buffer, 0); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Failed to read the LSPCON adapter info. RV = 0x%llx.", index, retVal); + return retVal; + } + + // Start to parse the adapter info + auto info = reinterpret_cast(buffer); + // Guard: Check whether this is a LSPCON adapter + if (!isLSPCONAdapter(info)) { + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Not a LSPCON DP-HDMI adapter. AdapterID = 0x%02x.", index, info->adapterID); + return kIOReturnNotFound; + } + + // Parse the chip vendor + char device[8] {}; + lilu_os_memcpy(device, info->deviceID, 6); + DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, Vendor::parse(info).getDescription(), device); + + // Parse the current adapter mode + Mode mode = Mode::parse(info->lspconCurrentMode); + DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] The current adapter mode is %s.", index, mode.getDescription()); + if (mode.isInvalid()) + SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); + return kIOReturnSuccess; +} + +IOReturn LSPCON::getMode(Mode &mode) { + IOReturn retVal = kIOReturnAborted; + + // Try to read the current mode from the adapter at most 5 times + for (int attempt = 0; attempt < 5; attempt++) { + // Read from the adapter @ 0x40; offset = 0x41 + uint8_t hwModeValue; + retVal = IGFX::AdvancedI2COverAUXSupport::advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CURRENT_MODE, 1, &hwModeValue, 0); + + // Guard: Can read the current adapter mode successfully + if (retVal == kIOReturnSuccess) { + DBGLOG("igfx", "SC: LSPCON::getMode() DInfo: [FB%d] The current mode value is 0x%02x.", index, hwModeValue); + mode = Mode::parse(hwModeValue); + break; + } + + // Sleep 1 ms just in case the adapter + // is busy processing other I2C requests + IOSleep(1); + } + + return retVal; +} + +IOReturn LSPCON::setMode(Mode newMode) { + // Guard: The given new mode must be valid + if (newMode.isInvalid()) + return kIOReturnAborted; + + // Guard: Write the new mode + uint8_t hwModeValue = newMode.getRawValue(); + IOReturn retVal = IGFX::AdvancedI2COverAUXSupport::advWriteI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CHANGE_MODE, 1, &hwModeValue, 0); + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to set the new adapter mode. RV = 0x%llx.", index, retVal); + return retVal; + } + + // Read the register again and verify the mode + uint32_t timeout = 200; + Mode mode; + while (timeout != 0) { + retVal = getMode(mode); + // Guard: Read the current effective mode + if (retVal != kIOReturnSuccess) { + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to read the new effective mode. RV = 0x%llx.", index, retVal); + continue; + } + // Guard: The new mode is effective now + if (mode == newMode) { + DBGLOG("igfx", "SC: LSPCON::setMode() DInfo: [FB%d] The new mode is now effective.", index); + return kIOReturnSuccess; + } + timeout -= 20; + IOSleep(20); + } + + SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Timed out while waiting for the new mode to be effective. Last RV = 0x%llx.", index, retVal); + return retVal; +} + +IOReturn LSPCON::setModeIfNecessary(Mode newMode) { + if (isRunningInMode(newMode)) { + DBGLOG("igfx", "SC: LSPCON::setModeIfNecessary() DInfo: [FB%d] The adapter is already running in %s mode. No need to update.", index, newMode.getDescription()); + return kIOReturnSuccess; + } + + return setMode(newMode); +} + +IOReturn LSPCON::wakeUpNativeAUX() { + uint8_t byte; + IOReturn retVal = IGFX::callbackIGFX->modLSPCONDriverSupport.orgReadAUX(controller, framebuffer, 0x00000, 1, &byte, displayPath); + if (retVal != kIOReturnSuccess) + SYSLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() Error: [FB%d] Failed to wake up the native AUX channel. RV = 0x%llx.", index, retVal); + else + DBGLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() DInfo: [FB%d] The native AUX channel is up. DPCD Rev = 0x%02x.", index, byte); + return retVal; +} + +bool LSPCON::isRunningInMode(Mode mode) +{ + Mode currentMode; + if (getMode(currentMode) != kIOReturnSuccess) { + DBGLOG("igfx", "LSPCON::isRunningInMode() Error: [FB%d] Failed to get the current adapter mode.", index); + return false; + } + return mode == currentMode; +} + +// MARK: - LSPCON Driver Support + + diff --git a/WhateverGreen/kern_igfx_lspcon.hpp b/WhateverGreen/kern_igfx_lspcon.hpp new file mode 100644 index 00000000..307e547c --- /dev/null +++ b/WhateverGreen/kern_igfx_lspcon.hpp @@ -0,0 +1,448 @@ +// +// kern_igfx_lspcon.hpp +// WhateverGreen +// +// Created by FireWolf on 9/21/20. +// Copyright © 2020 vit9696. All rights reserved. +// + +#ifndef kern_igfx_lspcon_hpp +#define kern_igfx_lspcon_hpp + +#include +#include +#include + +/** + * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 + * + * Specific to LSPCON DisplayPort 1.2 to HDMI 2.0 Adapter + */ +struct DisplayPortDualModeAdapterInfo { // first 128 bytes + /// [0x00] HDMI ID + /// + /// Fixed Value: "DP-HDMI ADAPTOR\x04" + uint8_t hdmiID[16]; + + /// [0x10] Adapter ID + /// + /// Bit Masks: + /// - isType2 = 0xA0 + /// - hasDPCD = 0x08 + /// + /// Sample Values: 0xA8 = Type 2 Adapter with DPCD + uint8_t adapterID; + + /// [0x11] IEEE OUI + /// + /// Sample Value: 0x001CF8 [Parade] + /// Reference: http://standards-oui.ieee.org/oui.txt + uint8_t oui[3]; + + /// [0x14] Device ID + /// + /// Sample Value: 0x505331373530 = "PS1750" + uint8_t deviceID[6]; + + /// [0x1A] Hardware Revision Number + /// + /// Sample Value: 0xB2 (B2 version) + uint8_t revision; + + /// [0x1B] Firmware Major Revision + uint8_t firmwareMajor; + + /// [0x1C] Firmware Minor Revision + uint8_t firmwareMinor; + + /// [0x1D] Maximum TMDS Clock + uint8_t maxTMDSClock; + + /// [0x1E] I2C Speed Capability + uint8_t i2cSpeedCap; + + /// [0x1F] Unused/Reserved Field??? + uint8_t reserved0; + + /// [0x20] TMDS Output Buffer State + /// + /// Bit Masks: + /// - Disabled = 0x01 + /// + /// Sample Value: + /// 0x00 = Enabled + uint8_t tmdsOutputBufferState; + + /// [0x21] HDMI PIN CONTROL + uint8_t hdmiPinCtrl; + + /// [0x22] I2C Speed Control + uint8_t i2cSpeedCtrl; + + /// [0x23 - 0x3F] Unused/Reserved Fields + uint8_t reserved1[29]; + + /// [0x40] [W] Set the new LSPCON mode + uint8_t lspconChangeMode; + + /// [0x41] [R] Get the current LSPCON mode + /// + /// Bit Masks: + /// - PCON = 0x01 + /// + /// Sample Value: + /// 0x00 = LS + /// 0x01 = PCON + uint8_t lspconCurrentMode; + + /// [0x42 - 0x7F] Rest Unused/Reserved Fields + uint8_t reserved2[62]; +}; + +/** + * Represents the onboard Level Shifter and Protocol Converter + */ +class LSPCON { +public: + /** + * Defines all possible adapter modes and convenient helpers to interpret a mode + * A wrapper to support member functions on an enumeration case + */ + struct Mode { + public: + /** + * Enumerates all possible values + */ + enum class Value : uint8_t { + /// Level Shifter Mode (DP++ to HDMI 1.4) + LevelShifter, + + /// Protocol Converter Mode (DP++ to HDMI 2.0) + ProtocolConverter, + + /// Invalid Mode + Invalid + }; + + /** + * Default constructor + */ + Mode(Value value = Value::Invalid) : value(value) {} + + /** + * [Mode Helper] Parse the adapter mode from the raw register value + * + * @param mode A raw register value read from the adapter. + * @return The corresponding adapter mode on success, `invalid` otherwise. + */ + static inline Mode parse(uint8_t mode) { + switch (mode & DP_DUAL_MODE_LSPCON_MODE_PCON) { + case DP_DUAL_MODE_LSPCON_MODE_LS: + return { Value::LevelShifter }; + + case DP_DUAL_MODE_LSPCON_MODE_PCON: + return { Value::ProtocolConverter }; + + default: + return { Value::Invalid }; + } + } + + /** + * Get the raw value of this adapter mode + * + * @param mode A valid adapter mode + * @return The corresponding register value on success. + * If the given mode is `Invalid`, the raw value of `LS` mode will be returned. + */ + inline uint8_t getRawValue() + { + switch (value) { + case Value::LevelShifter: + return DP_DUAL_MODE_LSPCON_MODE_LS; + + case Value::ProtocolConverter: + return DP_DUAL_MODE_LSPCON_MODE_PCON; + + default: + return DP_DUAL_MODE_LSPCON_MODE_LS; + } + } + + /** + * Get the string representation of this adapter mode + */ + inline const char *getDescription() { + switch (value) { + case Value::LevelShifter: + return "Level Shifter (DP++ to HDMI 1.4)"; + + case Value::ProtocolConverter: + return "Protocol Converter (DP++ to HDMI 2.0)"; + + default: + return "Invalid"; + } + } + + /** + * Check whether this is an invalid adapter mode + */ + inline bool isInvalid() { + return value == Value::Invalid; + } + + /** + * Check whether two modes are identical + */ + friend bool operator==(const Mode& lhs, const Mode& rhs) { return lhs.value == rhs.value; } + + private: + /** + * Register value when the adapter is in **Level Shifter** mode + */ + static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_LS = 0x00; + + /** + * Register value when the adapter is in **Protocol Converter** mode + */ + static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_PCON = 0x01; + + /** + * The underlying mode value + */ + Value value; + }; + + /** + * Defines all possible chip vendors and convenient helpers to parse and interpret a vendor + */ + struct Vendor { + public: + /** + * Enumerates all possible values + */ + enum class Value : uint32_t { + MegaChips, + Parade, + Unknown + }; + + /** + * Default constructor + */ + Vendor(Value value = Value::Unknown) : value(value) {} + + /** + * [Vendor Helper] Parse the adapter vendor from the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return The vendor on success, `Unknown` otherwise. + */ + static inline Vendor parse(DisplayPortDualModeAdapterInfo *info) { + uint32_t oui = info->oui[0] << 16 | info->oui[1] << 8 | info->oui[2]; + switch (oui) { + case DP_DUAL_MODE_LSPCON_VENDOR_PARADE: + return { Value::Parade }; + + case DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS: + return { Value::MegaChips }; + + default: + return { Value::Unknown }; + } + } + + /** + * [Vendor Helper] Get the string representation of the adapter vendor + */ + inline const char *getDescription() { + switch (value) { + case Value::Parade: + return "Parade"; + + case Value::MegaChips: + return "MegaChips"; + + default: + return "Unknown"; + } + } + + private: + /** + * IEEE OUI of Parade Technologies + */ + static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_PARADE = 0x001CF8; + + /** + * IEEE OUI of MegaChips Corporation + */ + static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS = 0x0060AD; + + /** + * The underlying vendor value + */ + Value value; + }; + + /** + * [Convenient] Create the LSPCON driver for the given framebuffer + * + * @param controller The opaque `AppleIntelFramebufferController` instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @return A driver instance on success, `nullptr` otherwise. + * @note This convenient initializer returns `nullptr` if it could not retrieve the framebuffer index. + */ + static LSPCON *create(void *controller, IORegistryEntry *framebuffer, void *displayPath); + + /** + * [Convenient] Destroy the LSPCON driver safely + * + * @param instance A nullable LSPCON driver instance + */ + static void deleter(LSPCON *instance NONNULL) { delete instance; } + + /** + * Probe the onboard LSPCON chip + * + * @return `kIOReturnSuccess` on success, errors otherwise + */ + IOReturn probe(); + + /** + * Get the current adapter mode + * + * @param mode The current adapter mode on return. + * @return `kIOReturnSuccess` on success, errors otherwise. + */ + IOReturn getMode(Mode &mode); + + /** + * Change the adapter mode + * + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method will not return until `newMode` is effective. + * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. + */ + IOReturn setMode(Mode newMode); + + /** + * Change the adapter mode if necessary + * + * @param newMode The new adapter mode + * @return `kIOReturnSuccess` on success, errors otherwise. + * @note This method is a wrapper of `setMode` and will only set the new mode if `newMode` is not currently effective. + * @seealso `setMode(newMode:)` + */ + IOReturn setModeIfNecessary(Mode newMode); + + /** + * Wake up the native DisplayPort AUX channel for this adapter + * + * @return `kIOReturnSuccess` on success, other errors otherwise. + */ + IOReturn wakeUpNativeAUX(); + + /** + * Return `true` if the adapter is running in the given mode + * + * @param mode The expected mode; one of `LS` and `PCON` + */ + bool isRunningInMode(Mode mode); + +private: + /// The 7-bit I2C slave address of the DisplayPort dual mode adapter + static constexpr uint32_t DP_DUAL_MODE_ADAPTER_I2C_ADDR = 0x40; + + /// Register address to change the adapter mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_CHANGE_MODE = 0x40; + + /// Register address to read the current adapter mode + static constexpr uint8_t DP_DUAL_MODE_LSPCON_CURRENT_MODE = 0x41; + + /// Bit mask indicating that the DisplayPort dual mode adapter is of type 2 + static constexpr uint8_t DP_DUAL_MODE_TYPE_IS_TYPE2 = 0xA0; + + /// Bit mask indicating that the DisplayPort dual mode adapter has DPCD (LSPCON case) + static constexpr uint8_t DP_DUAL_MODE_TYPE_HAS_DPCD = 0x08; + + /// The opaque framebuffer controller instance + void *controller; + + /// The framebuffer that owns this LSPCON chip + IORegistryEntry *framebuffer; + + /// The corresponding opaque display path instance + void *displayPath; + + /// The framebuffer index (for debugging purposes) + uint32_t index; + + /** + * Initialize the LSPCON chip for the given framebuffer + * + * @param controller The opaque `AppleIntelFramebufferController` instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @param index The framebuffer index (only for debugging purposes) + * @seealso LSPCON::create(controller:framebuffer:displayPath:) to create the driver instance. + */ + LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath, uint32_t index); + + /** + * [DP++ Helper] Check whether this is a HDMI adapter based on the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a HDMI adapter, `false` otherwise. + */ + static inline bool isHDMIAdapter(DisplayPortDualModeAdapterInfo *info) { + return memcmp(info->hdmiID, "DP-HDMI ADAPTOR\x04", 16) == 0; + } + + /** + * [DP++ Helper] Check whether this is a LSPCON adapter based on the adapter info + * + * @param info A non-null DP++ adapter info instance + * @return `true` if this is a LSPCON DP-HDMI adapter, `false` otherwise. + */ + static inline bool isLSPCONAdapter(DisplayPortDualModeAdapterInfo *info) { + // Guard: Check whether it is a DP to HDMI adapter + if (!isHDMIAdapter(info)) + return false; + + // Onboard LSPCON adapter must be of type 2 and have DPCD info + return info->adapterID == (DP_DUAL_MODE_TYPE_IS_TYPE2 | DP_DUAL_MODE_TYPE_HAS_DPCD); + } +}; + +/** + * Represents the LSPCON chip info for a framebuffer + */ +struct FramebufferLSPCON { + /** + * Indicate whether this framebuffer has an onboard LSPCON chip + * + * @note This value will be read from the IGPU property `framebuffer-conX-has-lspcon`. + * @warning If not specified, assuming no onboard LSPCON chip for this framebuffer. + */ + uint32_t hasLSPCON {0}; + + /** + * User preferred LSPCON adapter mode + * + * @note This value will be read from the IGPU property `framebuffer-conX-preferred-lspcon-mode`. + * @warning If not specified, assuming `PCON` mode is preferred. + * @warning If invalid mode value found, assuming `PCON` mode + */ + LSPCON::Mode preferredMode ;//{ LSPCON::Mode::Value::ProtocolConverter }; + + /** + * The corresponding LSPCON driver; `NULL` if no onboard chip + */ + LSPCON *lspcon {nullptr}; +}; + +#endif /* kern_igfx_lspcon_hpp */ From 140d766ca37f2fd0bc5608185c884343929d3948 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 16:40:23 -0700 Subject: [PATCH 084/102] LSPCON: Relocate the LSPCON driver support as a separate submodule. --- WhateverGreen/kern_igfx_lspcon.cpp | 178 +++++++++++++++++++++++++++++ WhateverGreen/kern_igfx_lspcon.hpp | 7 ++ 2 files changed, 185 insertions(+) diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index 0f4beda8..44b40b37 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -149,4 +149,182 @@ bool LSPCON::isRunningInMode(Mode mode) // MARK: - LSPCON Driver Support +void IGFX::LSPCONDriverSupport::init() { + // We only need to patch the framebuffer driver + requiresPatchingGraphics = false; + requiresPatchingFramebuffer = true; +} + +void IGFX::LSPCONDriverSupport::deinit() { + for (auto &con : lspcons) { + LSPCON::deleter(con.lspcon); + con.lspcon = nullptr; + } +} + +void IGFX::LSPCONDriverSupport::processKernel(KernelPatcher &patcher, DeviceInfo *info) { + // Enable the LSPCON driver support if the corresponding boot argument is found + enabled = checkKernelArgument("-igfxlspcon"); + // Or if "enable-lspcon-support" is set in IGPU property + if (!enabled) + enabled = info->videoBuiltin->getProperty("enable-lspcon-support") != nullptr; + + // Read the user-defined IGPU properties to know whether a connector has an onboard LSPCON chip + if (enabled) { + char name[48]; + uint32_t pmode = 0x01; // PCON mode as a fallback value + for (size_t index = 0; index < arrsize(lspcons); index++) { + bzero(name, sizeof(name)); + snprintf(name, sizeof(name), "framebuffer-con%lu-has-lspcon", index); + (void)WIOKit::getOSDataValue(info->videoBuiltin, name, lspcons[index].hasLSPCON); + snprintf(name, sizeof(name), "framebuffer-con%lu-preferred-lspcon-mode", index); + (void)WIOKit::getOSDataValue(info->videoBuiltin, name, pmode); + // Assuming PCON mode if invalid mode value (i.e. > 1) specified by the user + lspcons[index].preferredMode = ::LSPCON::Mode::parse(pmode != 0); // TODO: Remove :: + } + } +} + +void IGFX::LSPCONDriverSupport::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { + auto aux = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + auto gdi = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", address, size); + + if (aux && gdi) { + patcher.eraseCoverageInstPrefix(aux); + patcher.eraseCoverageInstPrefix(gdi); + orgReadAUX = reinterpret_cast(aux); + orgGetDPCDInfo = reinterpret_cast(patcher.routeFunction(gdi, reinterpret_cast(wrapGetDPCDInfo), true)); + if (orgReadAUX && orgGetDPCDInfo) { + DBGLOG("igfx", "SC: Functions have been routed successfully"); + } else { + patcher.clearError(); + SYSLOG("igfx", "SC: Failed to route functions."); + } + } else { + SYSLOG("igfx", "SC: Failed to find symbols."); + patcher.clearError(); + } +} + +void IGFX::LSPCONDriverSupport::setupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath) { + // Retrieve the framebuffer index + uint32_t index; + if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: Failed to retrieve the framebuffer index."); + return; + } + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); + + // Retrieve the user preference + LSPCON *lspcon = nullptr; + auto pmode = getLSPCONPreferredMode(index); +#ifdef DEBUG + framebuffer->setProperty("fw-framebuffer-has-lspcon", hasLSPCON(index)); + framebuffer->setProperty("fw-framebuffer-preferred-lspcon-mode", pmode.getRawValue(), 8); +#endif + // Guard: Check whether this framebuffer connector has an onboard LSPCON chip + if (!hasLSPCON(index)) { + // No LSPCON chip associated with this connector + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] No LSPCON chip associated with this framebuffer.", index); + return; + } + + // Guard: Check whether the LSPCON driver has already been initialized for this framebuffer + if (hasLSPCONInitialized(index)) { + // Already initialized + lspcon = getLSPCON(index); + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver (at 0x%llx) has already been initialized for this framebuffer.", index, lspcon); + // Confirm that the adapter is running in preferred mode + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to update the mode.", index); + } + // Wake up the native AUX channel if PCON mode is preferred + if (pmode.supportsHDMI20() && lspcon->wakeUpNativeAUX() != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to wake up the native AUX channel.", index); + } + return; + } + + // User has specified the existence of onboard LSPCON + // Guard: Initialize the driver for this framebuffer + lspcon = LSPCON::create(that, framebuffer, displayPath); + if (lspcon == nullptr) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to initialize the LSPCON driver.", index); + return; + } + + // Guard: Attempt to probe the onboard LSPCON chip + if (lspcon->probe() != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to probe the LSPCON adapter.", index); + LSPCON::deleter(lspcon); + lspcon = nullptr; + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Abort the LSPCON driver initialization.", index); + return; + } + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has detected the onboard chip successfully.", index); + + // LSPCON driver has been initialized successfully + setLSPCON(index, lspcon); + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has been initialized successfully.", index); + + // Guard: Set the preferred adapter mode if necessary + if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { + SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to set the %s mode.", index, pmode.getDescription()); + } + DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] The adapter is now running in preferred mode [%s].", index, pmode.getDescription()); +} + +IOReturn IGFX::LSPCONDriverSupport::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath) { + // + // Abstract + // + // Recent laptops are now typically equipped with a HDMI 2.0 port. + // For laptops with Intel IGPU + Nvidia DGPU, this HDMI 2.0 port could be either routed to IGPU or DGPU, + // and we are only interested in the former case, because DGPU (Optimus) is not supported on macOS. + // However, as indicated in Intel Product Specifications, Intel (U)HD Graphics card does not provide + // native HDMI 2.0 output. As a result, Intel suggests that OEMs should add an additional hardware component + // named LSPCON (Level Shifter and Protocol Converter) on the motherboard to convert DisplayPort to HDMI. + // + // LSPCON conforms to the DisplayPort Dual Mode (DP++) standard and works in either Level Shifter (LS) mode + // or Protocol Converter (PCON) mode. + // When the adapter is running in LS mode, it supports DisplayPort to HDMI 1.4. + // When the adapter is running in PCON mode, it supports DisplayPort to HDMI 2.0. + // LSPCON on some laptops, Dell XPS 15 9570 for example, might be configured in the firmware to run in LS mode + // by default, resulting in a black screen on a HDMI 2.0 connection. + // In comparison, LSPCON on a DisplayPort to HDMI 2.0 cable could be configured to always run in PCON mode. + // Fortunately, LSPCON is programmable and is a Type 2 DP++ adapter, so the graphics driver could communicate + // with the adapter via either the native I2C protocol or the special I2C-over-AUX protocol to configure the + // mode properly for HDMI connections. + // + // This reverse engineering research analyzes and exploits Apple's existing I2C-over-AUX transaction API layers, + // and an additional API layer is implemented to provide R/W access to registers at specific offsets on the adapter. + // AppleIntelFramebufferController::GetDPCDInfo() is then wrapped to inject code for LSPCON driver initialization, + // so that the adapter is configured to run in PCON mode instead on plugging the HDMI 2.0 cable to the port. + // Besides, the adapter is able to handle HDMI 1.4 connections when running in PCON mode, so we don't need to switch + // back to LS mode again. + // + // If you are interested in the detailed theory behind this fix, please take a look at my blog post. + // [ToDo: The article is still working in progress. I will add the link at here once I finish it. :D] + // + // Notes: + // 1. This fix is applicable for all laptops and PCs with HDMI 2.0 routed to IGPU. + // (Laptops/Mobiles start from Skylake platform. e.g. Intel Skull Canyon NUC; Iris Pro 580 and HDMI 2.0) + // 2. If your HDMI 2.0 with Intel (U)HD Graphics is working properly, you don't need this fix, + // as the adapter might already be configured in the firmware to run in PCON mode. + // + // - FireWolf + // - 2019.06 + // + + // Configure the LSPCON adapter for the given framebuffer + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Start to configure the LSPCON adapter."); + callbackIGFX->modLSPCONDriverSupport.setupLSPCON(that, framebuffer, displayPath); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Finished configuring the LSPCON adapter."); + + // Call the original method + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Will call the original method."); + IOReturn retVal = callbackIGFX->orgGetDPCDInfo(that, framebuffer, displayPath); + DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Returns 0x%llx.", retVal); + return retVal; +} diff --git a/WhateverGreen/kern_igfx_lspcon.hpp b/WhateverGreen/kern_igfx_lspcon.hpp index 307e547c..159c0f45 100644 --- a/WhateverGreen/kern_igfx_lspcon.hpp +++ b/WhateverGreen/kern_igfx_lspcon.hpp @@ -192,6 +192,13 @@ class LSPCON { return value == Value::Invalid; } + /** + * Check whether this mode supports HDMI 2.0 output + */ + inline bool supportsHDMI20() { + return value == Value::ProtocolConverter; + } + /** * Check whether two modes are identical */ From 3042c54cf2995119418e14dc5aef8c965e5f55c1 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 16:43:14 -0700 Subject: [PATCH 085/102] LSPCON: Add the submodule definition. --- WhateverGreen/kern_igfx.hpp | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 339e3cb2..87fd8ba2 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -979,7 +979,115 @@ class IGFX { void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; } modAdvancedI2COverAUXSupport; + /** + * A submodule that adds driver support for the onboard LSPCON chip to enable HDMI 2.0 output on SKL, KBL and CFL platforms + */ + class LSPCONDriverSupport: public PatchSubmodule { + public: + /** + * Original AppleIntelFramebufferController::ReadAUX function + */ + IOReturn (*orgReadAUX)(void *, IORegistryEntry *, uint32_t, uint16_t, void *, void *) {nullptr}; + + private: + /** + * Original AppleIntelFramebufferController::GetDPCDInfo function + * + * @seealso Refer to the document of `wrapGetDPCDInfo()` below. + */ + IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *) {nullptr}; + + /** + * User-defined LSPCON chip info for all possible framebuffers + */ + FramebufferLSPCON lspcons[MaxFramebufferConnectorCount]; + + /// MARK: Manage user-defined LSPCON chip info for all framebuffers + + /** + * Setup the LSPCON driver for the given framebuffer + * + * @param that The opaque framebuffer controller instance + * @param framebuffer The framebuffer that owns this LSPCON chip + * @param displayPath The corresponding opaque display path instance + * @note This method will update fields in `lspcons` accordingly on success. + */ + void setupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath); + + /** + * [Convenient] Check whether the given framebuffer has an onboard LSPCON chip + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. + */ + inline bool hasLSPCON(uint32_t index) { + return lspcons[index].hasLSPCON; + } + + /** + * [Convenient] Check whether the given framebuffer already has LSPCON driver initialized + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. + */ + inline bool hasLSPCONInitialized(uint32_t index) { + return lspcons[index].lspcon != nullptr; + } + + /** + * [Convenient] Get the non-null LSPCON driver associated with the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The LSPCON driver instance. + */ + inline LSPCON *getLSPCON(uint32_t index) { + return lspcons[index].lspcon; + } + + /** + * [Convenient] Set the non-null LSPCON driver for the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer + */ + inline void setLSPCON(uint32_t index, LSPCON *lspcon) { + lspcons[index].lspcon = lspcon; + } + + /** + * [Convenient] Get the preferred LSPCON mode for the given framebuffer + * + * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` + * @return The preferred adapter mode. + */ + inline LSPCON::Mode getLSPCONPreferredMode(uint32_t index) { + return lspcons[index].preferredMode; + } + + /** + * [Wrapper] Retrieve the DPCD info for a given framebuffer port + * + * @param that The hidden implicit `this` pointer + * @param framebuffer A framebuffer instance + * @param displayPath A display path instance + * @return `kIOReturnSuccess` on success, other values otherwise. + * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::GetDPCDInfo()` method. + * Used to inject code to initialize the driver for the onboard LSPCON chip. + */ + static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); + + public: + // MARK: Patch Submodule IMP + void init() override; + void deinit() override; + void processKernel(KernelPatcher &patcher, DeviceInfo *info) override; + void processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) override; + } modLSPCONDriverSupport; + /** + * The LSPCON driver requires access to I2C-over-AUX transaction APIs + */ + friend class LSPCON; /** * A collection of submodules From e3412f431abf3a19aa05183dd8f2843d1f91b0fa Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 16:50:19 -0700 Subject: [PATCH 086/102] LSPCON: Remove the old version. --- WhateverGreen/kern_igfx.cpp | 427 -------------------- WhateverGreen/kern_igfx.hpp | 609 +---------------------------- WhateverGreen/kern_igfx_lspcon.cpp | 2 +- 3 files changed, 4 insertions(+), 1034 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 6616cd88..b065091a 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -173,10 +173,6 @@ void IGFX::deinit() { // Deinitialize each submodule for (auto submodule : submodules) submodule->deinit(); - for (auto &con : lspcons) { - LSPCON::deleter(con.lspcon); - con.lspcon = nullptr; - } } void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { @@ -251,30 +247,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { } else { fwLoadMode = FW_APPLE; /* Do nothing, GuC is either unsupported due to low OS or Apple */ } - - // Enable the LSPCON driver support if the corresponding boot argument is found - supportLSPCON = checkKernelArgument("-igfxlspcon"); - // Or if "enable-lspcon-support" is set in IGPU property - if (!supportLSPCON) - supportLSPCON = info->videoBuiltin->getProperty("enable-lspcon-support") != nullptr; - - // Read the user-defined IGPU properties to know whether a connector has an onboard LSPCON chip - if (supportLSPCON) { - char name[48]; - uint32_t pmode = 0x01; // PCON mode as a fallback value - for (size_t index = 0; index < arrsize(lspcons); index++) { - bzero(name, sizeof(name)); - snprintf(name, sizeof(name), "framebuffer-con%lu-has-lspcon", index); - (void)WIOKit::getOSDataValue(info->videoBuiltin, name, lspcons[index].hasLSPCON); - snprintf(name, sizeof(name), "framebuffer-con%lu-preferred-lspcon-mode", index); - (void)WIOKit::getOSDataValue(info->videoBuiltin, name, pmode); - // Assuming PCON mode if invalid mode value (i.e. > 1) specified by the user - lspcons[index].preferredMode = LSPCON::parseMode(pmode != 0); - } - } - - // Enable the verbose output in I2C-over-AUX transactions if the corresponding boot argument is found - verboseI2C = checkKernelArgument("-igfxi2cdbg"); // Iterate through each submodule and redirect the request for (auto submodule : submodules) @@ -355,8 +327,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { return true; if (cflBacklightPatch != CoffeeBacklightPatch::Off) return true; - if (supportLSPCON) - return true; if (forceCompleteModeset.enable) return true; if (forceOnlineDisplay.enable) @@ -573,29 +543,6 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (debugFramebuffer) loadFramebufferDebug(patcher, index, address, size); - if (supportLSPCON) { - auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); - auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); - auto gdi = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", address, size); - if (roa && woa && gdi) { - patcher.eraseCoverageInstPrefix(roa); - patcher.eraseCoverageInstPrefix(woa); - patcher.eraseCoverageInstPrefix(gdi); - orgReadI2COverAUX = reinterpret_cast(patcher.routeFunction(roa, reinterpret_cast(wrapReadI2COverAUX), true)); - orgWriteI2COverAUX = reinterpret_cast(patcher.routeFunction(woa, reinterpret_cast(wrapWriteI2COverAUX), true)); - orgGetDPCDInfo = reinterpret_cast(patcher.routeFunction(gdi, reinterpret_cast(wrapGetDPCDInfo), true)); - if (orgReadI2COverAUX && orgWriteI2COverAUX && orgGetDPCDInfo) { - DBGLOG("igfx", "SC: ReadI2COverAUX(), etc. have been routed successfully"); - } else { - patcher.clearError(); - SYSLOG("igfx", "SC: ReadI2COverAUX(), etc. cannot be routed"); - } - } else { - SYSLOG("igfx", "SC: Failed to find ReadI2COverAUX(), etc"); - patcher.clearError(); - } - } - if (blackScreenPatch) { bool foundSymbol = false; @@ -938,380 +885,6 @@ uint32_t IGFX::wrapGetDisplayStatus(IOService *framebuffer, void *displayPath) { return ret; } -IOReturn IGFX::LSPCON::probe() { - // Read the adapter info - uint8_t buffer[128] {}; - IOReturn retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, 0x00, 128, buffer, 0); - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Failed to read the LSPCON adapter info. RV = 0x%llx.", index, retVal); - return retVal; - } - - // Start to parse the adapter info - auto info = reinterpret_cast(buffer); - // Guard: Check whether this is a LSPCON adapter - if (!isLSPCONAdapter(info)) { - SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Not a LSPCON DP-HDMI adapter. AdapterID = 0x%02x.", index, info->adapterID); - return kIOReturnNotFound; - } - - // Parse the chip vendor - char device[8] {}; - lilu_os_memcpy(device, info->deviceID, 6); - DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] Found the LSPCON adapter: %s %s.", index, getVendorString(parseVendor(info)), device); - - // Parse the current adapter mode - Mode mode = parseMode(info->lspconCurrentMode); - DBGLOG("igfx", "SC: LSPCON::probe() DInfo: [FB%d] The current adapter mode is %s.", index, getModeString(mode)); - if (mode == Mode::Invalid) - SYSLOG("igfx", "SC: LSPCON::probe() Error: [FB%d] Cannot detect the current adapter mode. Assuming Level Shifter mode.", index); - return kIOReturnSuccess; -} - -IOReturn IGFX::LSPCON::getMode(Mode *mode) { - IOReturn retVal = kIOReturnAborted; - - // Guard: The given `mode` pointer cannot be NULL - if (mode != nullptr) { - // Try to read the current mode from the adapter at most 5 times - for (int attempt = 0; attempt < 5; attempt++) { - // Read from the adapter @ 0x40; offset = 0x41 - uint8_t hwModeValue; - retVal = callbackIGFX->advReadI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CURRENT_MODE, 1, &hwModeValue, 0); - - // Guard: Can read the current adapter mode successfully - if (retVal == kIOReturnSuccess) { - DBGLOG("igfx", "SC: LSPCON::getMode() DInfo: [FB%d] The current mode value is 0x%02x.", index, hwModeValue); - *mode = parseMode(hwModeValue); - break; - } - - // Sleep 1 ms just in case the adapter - // is busy processing other I2C requests - IOSleep(1); - } - } - - return retVal; -} - -IOReturn IGFX::LSPCON::setMode(Mode newMode) { - // Guard: The given new mode must be valid - if (newMode == Mode::Invalid) - return kIOReturnAborted; - - // Guard: Write the new mode - uint8_t hwModeValue = getModeValue(newMode); - IOReturn retVal = callbackIGFX->advWriteI2COverAUX(controller, framebuffer, displayPath, DP_DUAL_MODE_ADAPTER_I2C_ADDR, DP_DUAL_MODE_LSPCON_CHANGE_MODE, 1, &hwModeValue, 0); - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to set the new adapter mode. RV = 0x%llx.", index, retVal); - return retVal; - } - - // Read the register again and verify the mode - uint32_t timeout = 200; - Mode mode = Mode::Invalid; - while (timeout != 0) { - retVal = getMode(&mode); - // Guard: Read the current effective mode - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Failed to read the new effective mode. RV = 0x%llx.", index, retVal); - continue; - } - // Guard: The new mode is effective now - if (mode == newMode) { - DBGLOG("igfx", "SC: LSPCON::setMode() DInfo: [FB%d] The new mode is now effective.", index); - return kIOReturnSuccess; - } - timeout -= 20; - IOSleep(20); - } - - SYSLOG("igfx", "SC: LSPCON::setMode() Error: [FB%d] Timed out while waiting for the new mode to be effective. Last RV = 0x%llx.", index, retVal); - return retVal; -} - -IOReturn IGFX::LSPCON::setModeIfNecessary(Mode newMode) { - if (isRunningInMode(newMode)) { - DBGLOG("igfx", "SC: LSPCON::setModeIfNecessary() DInfo: [FB%d] The adapter is already running in %s mode. No need to update.", index, getModeString(newMode)); - return kIOReturnSuccess; - } - - return setMode(newMode); -} - -IOReturn IGFX::LSPCON::wakeUpNativeAUX() { - uint8_t byte; - IOReturn retVal = wrapReadAUX(controller, framebuffer, 0x00000, 1, &byte, displayPath); - if (retVal != kIOReturnSuccess) - SYSLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() Error: [FB%d] Failed to wake up the native AUX channel. RV = 0x%llx.", index, retVal); - else - DBGLOG("igfx", "SC: LSPCON::wakeUpNativeAUX() DInfo: [FB%d] The native AUX channel is up. DPCD Rev = 0x%02x.", index, byte); - return retVal; -} - -IOReturn IGFX::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { - if (callbackIGFX->verboseI2C) { - uint32_t index = 0xFF; - AppleIntelFramebufferExplorer::getIndex(framebuffer, index); - DBGLOG("igfx", "SC: ReadI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = %d.", - index, address, length, intermediate, flags); - IOReturn retVal = callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); - DBGLOG("igfx", "SC: ReadI2COverAUX() returns 0x%x.", retVal); - return retVal; - } else { - return callbackIGFX->orgReadI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate, flags); - } -} - -IOReturn IGFX::wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate) { - if (callbackIGFX->verboseI2C) { - uint32_t index = 0xFF; - AppleIntelFramebufferExplorer::getIndex(framebuffer, index); - DBGLOG("igfx", "SC: WriteI2COverAUX() called. FB%d: Addr = 0x%02x; Len = %02d; MOT = %d; Flags = 0", - index, address, length, intermediate); - IOReturn retVal = callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); - DBGLOG("igfx", "SC: WriteI2COverAUX() returns 0x%x.", retVal); - return retVal; - } else { - return callbackIGFX->orgWriteI2COverAUX(that, framebuffer, displayPath, address, length, buffer, intermediate); - } -} - -IOReturn IGFX::advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags) { - // No need to check the given `address` and `offset` - // if they are invalid, the underlying RunAUXCommand() will return an error - // First start the transaction by performing an empty write - IOReturn retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, true); - - // Guard: Check the START transaction - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: AdvSeekI2COverAUX() Error: Failed to start the I2C transaction. Return value = 0x%x.\n", retVal); - return retVal; - } - - // Write a single byte to the given I2C slave - // and set the Middle-of-Transaction bit to 1 - return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 1, reinterpret_cast(&offset), true); -} - -IOReturn IGFX::advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { - // Guard: Check the buffer length - if (length == 0) { - SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Buffer length must be non-zero."); - return kIOReturnInvalid; - } - - // Guard: Check the buffer - if (buffer == nullptr) { - SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Buffer cannot be NULL."); - return kIOReturnInvalid; - } - - // Guard: Start the transaction and set the access offset successfully - IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: AdvReadI2COverAUX() Error: Failed to set the data offset."); - return retVal; - } - - // Process the read request - // ReadI2COverAUX() can only process up to 16 bytes in one AUX transaction - // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header - while (length != 0) { - // Calculate the new length for this I2C-over-AUX transaction - uint16_t newLength = length >= 16 ? 16 : length; - - // This is an intermediate transaction - retVal = wrapReadI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true, flags); - - // Guard: The intermediate transaction succeeded - if (retVal != kIOReturnSuccess) { - // Terminate the transaction - wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); - return retVal; - } - - // Update the buffer position and length - length -= newLength; - buffer += newLength; - } - - // All intermediate transactions succeeded - // Terminate the transaction - return wrapReadI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false, flags); -} - -IOReturn IGFX::advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags) { - // Guard: Check the buffer length - if (length == 0) { - SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Buffer length must be non-zero."); - return kIOReturnInvalid; - } - - // Guard: Check the buffer - if (buffer == nullptr) { - SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Buffer cannot be NULL."); - return kIOReturnInvalid; - } - - // Guard: Start the transaction and set the access offset successfully - IOReturn retVal = advSeekI2COverAUX(that, framebuffer, displayPath, address, offset, flags); - if (retVal != kIOReturnSuccess) { - SYSLOG("igfx", "SC: AdvWriteI2COverAUX() Error: Failed to set the data offset."); - return retVal; - } - - // Process the write request - // WriteI2COverAUX() can only process up to 16 bytes in one AUX transaction - // because the burst data size is 20 bytes, in which the first 4 bytes are used for the AUX message header - while (length != 0) { - // Calculate the new length for this I2C-over-AUX transaction - uint16_t newLength = length >= 16 ? 16 : length; - - // This is an intermediate transaction - retVal = wrapWriteI2COverAUX(that, framebuffer, displayPath, address, newLength, buffer, true); - - // Guard: The intermediate transaction succeeded - if (retVal != kIOReturnSuccess) { - // Terminate the transaction - wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); - return retVal; - } - - // Update the buffer position and length - length -= newLength; - buffer += newLength; - } - - // All intermediate transactions succeeded - // Terminate the transaction - return wrapWriteI2COverAUX(that, framebuffer, displayPath, address, 0, nullptr, false); -} - -void IGFX::framebufferSetupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath) { - // Retrieve the framebuffer index - uint32_t index; - if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: Failed to retrieve the framebuffer index."); - return; - } - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] called with controller at 0x%llx and framebuffer at 0x%llx.", index, that, framebuffer); - - // Retrieve the user preference - LSPCON *lspcon = nullptr; - auto pmode = framebufferLSPCONGetPreferredMode(index); -#ifdef DEBUG - framebuffer->setProperty("fw-framebuffer-has-lspcon", framebufferHasLSPCON(index)); - framebuffer->setProperty("fw-framebuffer-preferred-lspcon-mode", LSPCON::getModeValue(pmode), 8); -#endif - - // Guard: Check whether this framebuffer connector has an onboard LSPCON chip - if (!framebufferHasLSPCON(index)) { - // No LSPCON chip associated with this connector - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] No LSPCON chip associated with this framebuffer.", index); - return; - } - - // Guard: Check whether the LSPCON driver has already been initialized for this framebuffer - if (framebufferHasLSPCONInitialized(index)) { - // Already initialized - lspcon = framebufferGetLSPCON(index); - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver (at 0x%llx) has already been initialized for this framebuffer.", index, lspcon); - // Confirm that the adapter is running in preferred mode - if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to update the mode.", index); - } - // Wake up the native AUX channel if PCON mode is preferred - if (pmode == LSPCON::Mode::ProtocolConverter && lspcon->wakeUpNativeAUX() != kIOReturnSuccess) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to wake up the native AUX channel.", index); - } - return; - } - - // User has specified the existence of onboard LSPCON - // Guard: Initialize the driver for this framebuffer - lspcon = LSPCON::create(that, framebuffer, displayPath); - if (lspcon == nullptr) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to initialize the LSPCON driver.", index); - return; - } - - // Guard: Attempt to probe the onboard LSPCON chip - if (lspcon->probe() != kIOReturnSuccess) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Failed to probe the LSPCON adapter.", index); - LSPCON::deleter(lspcon); - lspcon = nullptr; - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] Abort the LSPCON driver initialization.", index); - return; - } - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has detected the onboard chip successfully.", index); - - // LSPCON driver has been initialized successfully - framebufferSetLSPCON(index, lspcon); - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] LSPCON driver has been initialized successfully.", index); - - // Guard: Set the preferred adapter mode if necessary - if (lspcon->setModeIfNecessary(pmode) != kIOReturnSuccess) { - SYSLOG("igfx", "SC: fbSetupLSPCON() Error: [FB%d] The adapter is not running in preferred mode. Failed to set the %s mode.", index, LSPCON::getModeString(pmode)); - } - DBGLOG("igfx", "SC: fbSetupLSPCON() DInfo: [FB%d] The adapter is now running in preferred mode [%s].", index, LSPCON::getModeString(pmode)); -} - -IOReturn IGFX::wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath) { - // - // Abstract - // - // Recent laptops are now typically equipped with a HDMI 2.0 port. - // For laptops with Intel IGPU + Nvidia DGPU, this HDMI 2.0 port could be either routed to IGPU or DGPU, - // and we are only interested in the former case, because DGPU (Optimus) is not supported on macOS. - // However, as indicated in Intel Product Specifications, Intel (U)HD Graphics card does not provide - // native HDMI 2.0 output. As a result, Intel suggests that OEMs should add an additional hardware component - // named LSPCON (Level Shifter and Protocol Converter) on the motherboard to convert DisplayPort to HDMI. - // - // LSPCON conforms to the DisplayPort Dual Mode (DP++) standard and works in either Level Shifter (LS) mode - // or Protocol Converter (PCON) mode. - // When the adapter is running in LS mode, it supports DisplayPort to HDMI 1.4. - // When the adapter is running in PCON mode, it supports DisplayPort to HDMI 2.0. - // LSPCON on some laptops, Dell XPS 15 9570 for example, might be configured in the firmware to run in LS mode - // by default, resulting in a black screen on a HDMI 2.0 connection. - // In comparison, LSPCON on a DisplayPort to HDMI 2.0 cable could be configured to always run in PCON mode. - // Fortunately, LSPCON is programmable and is a Type 2 DP++ adapter, so the graphics driver could communicate - // with the adapter via either the native I2C protocol or the special I2C-over-AUX protocol to configure the - // mode properly for HDMI connections. - // - // This reverse engineering research analyzes and exploits Apple's existing I2C-over-AUX transaction API layers, - // and an additional API layer is implemented to provide R/W access to registers at specific offsets on the adapter. - // AppleIntelFramebufferController::GetDPCDInfo() is then wrapped to inject code for LSPCON driver initialization, - // so that the adapter is configured to run in PCON mode instead on plugging the HDMI 2.0 cable to the port. - // Besides, the adapter is able to handle HDMI 1.4 connections when running in PCON mode, so we don't need to switch - // back to LS mode again. - // - // If you are interested in the detailed theory behind this fix, please take a look at my blog post. - // [ToDo: The article is still working in progress. I will add the link at here once I finish it. :D] - // - // Notes: - // 1. This fix is applicable for all laptops and PCs with HDMI 2.0 routed to IGPU. - // (Laptops/Mobiles start from Skylake platform. e.g. Intel Skull Canyon NUC; Iris Pro 580 and HDMI 2.0) - // 2. If your HDMI 2.0 with Intel (U)HD Graphics is working properly, you don't need this fix, - // as the adapter might already be configured in the firmware to run in PCON mode. - // - // - FireWolf - // - 2019.06 - // - - // Configure the LSPCON adapter for the given framebuffer - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Start to configure the LSPCON adapter."); - framebufferSetupLSPCON(that, framebuffer, displayPath); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Finished configuring the LSPCON adapter."); - - // Call the original method - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Will call the original method."); - IOReturn retVal = callbackIGFX->orgGetDPCDInfo(that, framebuffer, displayPath); - DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Returns 0x%llx.", retVal); - return retVal; -} - void IGFX::wrapCflWriteRegister32(void *that, uint32_t reg, uint32_t value) { if (reg == BXT_BLC_PWM_FREQ1) { if (value && value != callbackIGFX->driverBacklightFrequency) { diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 87fd8ba2..2fa68a3a 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -9,6 +9,7 @@ #define kern_igfx_hpp #include "kern_fb.hpp" +#include "kern_igfx_lspcon.hpp" #include #include @@ -303,21 +304,6 @@ class IGFX { void (*orgCflWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; void (*orgKblWriteRegister32)(void *, uint32_t, uint32_t) {nullptr}; - /** - * Original AppleIntelFramebufferController::ReadI2COverAUX function - */ - IOReturn (*orgReadI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool, uint8_t) {nullptr}; - - /** - * Original AppleIntelFramebufferController::WriteI2COverAUX function - */ - IOReturn (*orgWriteI2COverAUX)(void *, IORegistryEntry *, void *, uint32_t, uint16_t, uint8_t *, bool) {nullptr}; - - /** - * Original AppleIntelFramebufferController::GetDPCDInfo function - */ - IOReturn (*orgGetDPCDInfo)(void *, IORegistryEntry *, void *) {nullptr}; - /** * Set to true if a black screen ComputeLaneCount patch is required */ @@ -340,16 +326,6 @@ class IGFX { */ CoffeeBacklightPatch cflBacklightPatch {CoffeeBacklightPatch::Off}; - /** - * Set to true to enable LSPCON driver support - */ - bool supportLSPCON {false}; - - /** - * Set to true to enable verbose output in I2C-over-AUX transactions - */ - bool verboseI2C {false}; - /** * Set to true if PAVP code should be disabled */ @@ -846,7 +822,7 @@ class IGFX { /** * A submodule to support advanced I2C-over-AUX transactions on SKL, KBL and CFL platforms * - * @note LSPCON driver enables this module automatically. + * @note LSPCON driver enables this module automatically. This module must be placed after the LSPCON module. * @note This module is not compatible with ICL platforms yet. * Not sure if Ice Lake graphics card supports native HDMI 2.0 output. */ @@ -1092,7 +1068,7 @@ class IGFX { /** * A collection of submodules */ - PatchSubmodule *submodules[4] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix, &modCoreDisplayClockFix, &modHDMIDividersCalcFix }; + PatchSubmodule *submodules[6] = { &modDVMTCalcFix, &modDPCDMaxLinkRateFix, &modCoreDisplayClockFix, &modHDMIDividersCalcFix, &modLSPCONDriverSupport, &modAdvancedI2COverAUXSupport }; /** * Ensure each modeset is a complete modeset. @@ -1228,585 +1204,6 @@ class IGFX { return false; } }; - - /** - * Represents the register layouts of DisplayPort++ adapter at I2C address 0x40 - * - * Specific to LSPCON DisplayPort 1.2 to HDMI 2.0 Adapter - */ - struct DisplayPortDualModeAdapterInfo { // first 128 bytes - /// [0x00] HDMI ID - /// - /// Fixed Value: "DP-HDMI ADAPTOR\x04" - uint8_t hdmiID[16]; - - /// [0x10] Adapter ID - /// - /// Bit Masks: - /// - isType2 = 0xA0 - /// - hasDPCD = 0x08 - /// - /// Sample Values: 0xA8 = Type 2 Adapter with DPCD - uint8_t adapterID; - - /// [0x11] IEEE OUI - /// - /// Sample Value: 0x001CF8 [Parade] - /// Reference: http://standards-oui.ieee.org/oui.txt - uint8_t oui[3]; - - /// [0x14] Device ID - /// - /// Sample Value: 0x505331373530 = "PS1750" - uint8_t deviceID[6]; - - /// [0x1A] Hardware Revision Number - /// - /// Sample Value: 0xB2 (B2 version) - uint8_t revision; - - /// [0x1B] Firmware Major Revision - uint8_t firmwareMajor; - - /// [0x1C] Firmware Minor Revision - uint8_t firmwareMinor; - - /// [0x1D] Maximum TMDS Clock - uint8_t maxTMDSClock; - - /// [0x1E] I2C Speed Capability - uint8_t i2cSpeedCap; - - /// [0x1F] Unused/Reserved Field??? - uint8_t reserved0; - - /// [0x20] TMDS Output Buffer State - /// - /// Bit Masks: - /// - Disabled = 0x01 - /// - /// Sample Value: - /// 0x00 = Enabled - uint8_t tmdsOutputBufferState; - - /// [0x21] HDMI PIN CONTROL - uint8_t hdmiPinCtrl; - - /// [0x22] I2C Speed Control - uint8_t i2cSpeedCtrl; - - /// [0x23 - 0x3F] Unused/Reserved Fields - uint8_t reserved1[29]; - - /// [0x40] [W] Set the new LSPCON mode - uint8_t lspconChangeMode; - - /// [0x41] [R] Get the current LSPCON mode - /// - /// Bit Masks: - /// - PCON = 0x01 - /// - /// Sample Value: - /// 0x00 = LS - /// 0x01 = PCON - uint8_t lspconCurrentMode; - - /// [0x42 - 0x7F] Rest Unused/Reserved Fields - uint8_t reserved2[62]; - }; - - /** - * Represents the onboard Level Shifter and Protocol Converter - */ - class LSPCON { - public: - /** - * Represents all possible adapter modes - */ - enum class Mode: uint32_t { - - /// Level Shifter Mode (DP++ to HDMI 1.4) - LevelShifter = 0x00, - - /// Protocol Converter Mode (DP++ to HDMI 2.0) - ProtocolConverter = 0x01, - - /// Invalid Mode - Invalid - }; - - /** - * [Mode Helper] Parse the adapter mode from the raw register value - * - * @param mode A raw register value read from the adapter. - * @return The corresponding adapter mode on success, `invalid` otherwise. - */ - static inline Mode parseMode(uint8_t mode) { - switch (mode & DP_DUAL_MODE_LSPCON_MODE_PCON) { - case DP_DUAL_MODE_LSPCON_MODE_LS: - return Mode::LevelShifter; - - case DP_DUAL_MODE_LSPCON_MODE_PCON: - return Mode::ProtocolConverter; - - default: - return Mode::Invalid; - } - } - - /** - * [Mode Helper] Get the raw value of the given adapter mode - * - * @param mode A valid adapter mode - * @return The corresponding register value on success. - * If the given mode is `Invalid`, the raw value of `LS` mode will be returned. - */ - static inline uint8_t getModeValue(Mode mode) { - switch (mode) { - case Mode::LevelShifter: - return DP_DUAL_MODE_LSPCON_MODE_LS; - - case Mode::ProtocolConverter: - return DP_DUAL_MODE_LSPCON_MODE_PCON; - - default: - return DP_DUAL_MODE_LSPCON_MODE_LS; - } - } - - /** - * [Mode Helper] Get the string representation of the adapter mode - */ - static inline const char *getModeString(Mode mode) { - switch (mode) { - case Mode::LevelShifter: - return "Level Shifter (DP++ to HDMI 1.4)"; - - case Mode::ProtocolConverter: - return "Protocol Converter (DP++ to HDMI 2.0)"; - - default: - return "Invalid"; - } - } - - /** - * [Convenient] Create the LSPCON driver for the given framebuffer - * - * @param controller The opaque `AppleIntelFramebufferController` instance - * @param framebuffer The framebuffer that owns this LSPCON chip - * @param displayPath The corresponding opaque display path instance - * @return A driver instance on success, `nullptr` otherwise. - * @note This convenient initializer returns `nullptr` if it could not retrieve the framebuffer index. - */ - static LSPCON *create(void *controller, IORegistryEntry *framebuffer, void *displayPath) { - // Guard: Framebuffer index should exist - uint32_t index; - if (!AppleIntelFramebufferExplorer::getIndex(framebuffer, index)) - return nullptr; - - // Call the private constructor - return new LSPCON(controller, framebuffer, displayPath, index); - } - - /** - * [Convenient] Destroy the LSPCON driver safely - * - * @param instance A nullable LSPCON driver instance - */ - static void deleter(LSPCON *instance NONNULL) { delete instance; } - - /** - * Probe the onboard LSPCON chip - * - * @return `kIOReturnSuccess` on success, errors otherwise - */ - IOReturn probe(); - - /** - * Get the current adapter mode - * - * @param mode The current adapter mode on return. - * @return `kIOReturnSuccess` on success, errors otherwise. - */ - IOReturn getMode(Mode *mode); - - /** - * Change the adapter mode - * - * @param newMode The new adapter mode - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method will not return until `newMode` is effective. - * @warning This method will return the result of the last attempt if timed out on waiting for `newMode` to be effective. - */ - IOReturn setMode(Mode newMode); - - /** - * Change the adapter mode if necessary - * - * @param newMode The new adapter mode - * @return `kIOReturnSuccess` on success, errors otherwise. - * @note This method is a wrapper of `setMode` and will only set the new mode if `newMode` is not currently effective. - * @seealso `setMode(newMode:)` - */ - IOReturn setModeIfNecessary(Mode newMode); - - /** - * Wake up the native DisplayPort AUX channel for this adapter - * - * @return `kIOReturnSuccess` on success, other errors otherwise. - */ - IOReturn wakeUpNativeAUX(); - - /** - * Return `true` if the adapter is running in the given mode - * - * @param mode The expected mode; one of `LS` and `PCON` - */ - inline bool isRunningInMode(Mode mode) { - Mode currentMode; - if (getMode(¤tMode) != kIOReturnSuccess) { - DBGLOG("igfx", "LSPCON::isRunningInMode() Error: [FB%d] Failed to get the current adapter mode.", index); - return false; - } - return mode == currentMode; - } - - private: - /// The 7-bit I2C slave address of the DisplayPort dual mode adapter - static constexpr uint32_t DP_DUAL_MODE_ADAPTER_I2C_ADDR = 0x40; - - /// Register address to change the adapter mode - static constexpr uint8_t DP_DUAL_MODE_LSPCON_CHANGE_MODE = 0x40; - - /// Register address to read the current adapter mode - static constexpr uint8_t DP_DUAL_MODE_LSPCON_CURRENT_MODE = 0x41; - - /// Register value when the adapter is in **Level Shifter** mode - static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_LS = 0x00; - - /// Register value when the adapter is in **Protocol Converter** mode - static constexpr uint8_t DP_DUAL_MODE_LSPCON_MODE_PCON = 0x01; - - /// IEEE OUI of Parade Technologies - static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_PARADE = 0x001CF8; - - /// IEEE OUI of MegaChips Corporation - static constexpr uint32_t DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS = 0x0060AD; - - /// Bit mask indicating that the DisplayPort dual mode adapter is of type 2 - static constexpr uint8_t DP_DUAL_MODE_TYPE_IS_TYPE2 = 0xA0; - - /// Bit mask indicating that the DisplayPort dual mode adapter has DPCD (LSPCON case) - static constexpr uint8_t DP_DUAL_MODE_TYPE_HAS_DPCD = 0x08; - - /** - * Represents all possible chip vendors - */ - enum class Vendor { - MegaChips, - Parade, - Unknown - }; - - /// The opaque framebuffer controller instance - void *controller; - - /// The framebuffer that owns this LSPCON chip - IORegistryEntry *framebuffer; - - /// The corresponding opaque display path instance - void *displayPath; - - /// The framebuffer index (for debugging purposes) - uint32_t index; - - /** - * Initialize the LSPCON chip for the given framebuffer - * - * @param controller The opaque `AppleIntelFramebufferController` instance - * @param framebuffer The framebuffer that owns this LSPCON chip - * @param displayPath The corresponding opaque display path instance - * @param index The framebuffer index (only for debugging purposes) - * @seealso LSPCON::create(controller:framebuffer:displayPath:) to create the driver instance. - */ - LSPCON(void *controller, IORegistryEntry *framebuffer, void *displayPath, uint32_t index) { - this->controller = controller; - this->framebuffer = framebuffer; - this->displayPath = displayPath; - this->index = index; - } - - /** - * [Vendor Helper] Parse the adapter vendor from the adapter info - * - * @param info A non-null DP++ adapter info instance - * @return The vendor on success, `Unknown` otherwise. - */ - static inline Vendor parseVendor(DisplayPortDualModeAdapterInfo *info) { - uint32_t oui = info->oui[0] << 16 | info->oui[1] << 8 | info->oui[2]; - switch (oui) { - case DP_DUAL_MODE_LSPCON_VENDOR_PARADE: - return Vendor::Parade; - - case DP_DUAL_MODE_LSPCON_VENDOR_MEGACHIPS: - return Vendor::MegaChips; - - default: - return Vendor::Unknown; - } - } - - /** - * [Vendor Helper] Get the string representation of the adapter vendor - */ - static inline const char *getVendorString(Vendor vendor) { - switch (vendor) { - case Vendor::Parade: - return "Parade"; - - case Vendor::MegaChips: - return "MegaChips"; - - default: - return "Unknown"; - } - } - - /** - * [DP++ Helper] Check whether this is a HDMI adapter based on the adapter info - * - * @param info A non-null DP++ adapter info instance - * @return `true` if this is a HDMI adapter, `false` otherwise. - */ - static inline bool isHDMIAdapter(DisplayPortDualModeAdapterInfo *info) { - return memcmp(info->hdmiID, "DP-HDMI ADAPTOR\x04", 16) == 0; - } - - /** - * [DP++ Helper] Check whether this is a LSPCON adapter based on the adapter info - * - * @param info A non-null DP++ adapter info instance - * @return `true` if this is a LSPCON DP-HDMI adapter, `false` otherwise. - */ - static inline bool isLSPCONAdapter(DisplayPortDualModeAdapterInfo *info) { - // Guard: Check whether it is a DP to HDMI adapter - if (!isHDMIAdapter(info)) - return false; - - // Onboard LSPCON adapter must be of type 2 and have DPCD info - return info->adapterID == (DP_DUAL_MODE_TYPE_IS_TYPE2 | DP_DUAL_MODE_TYPE_HAS_DPCD); - } - }; - - /** - * Represents the LSPCON chip info for a framebuffer - */ - struct FramebufferLSPCON { - /** - * Indicate whether this framebuffer has an onboard LSPCON chip - * - * @note This value will be read from the IGPU property `framebuffer-conX-has-lspcon`. - * @warning If not specified, assuming no onboard LSPCON chip for this framebuffer. - */ - uint32_t hasLSPCON {0}; - - /** - * User preferred LSPCON adapter mode - * - * @note This value will be read from the IGPU property `framebuffer-conX-preferred-lspcon-mode`. - * @warning If not specified, assuming `PCON` mode is preferred. - * @warning If invalid mode value found, assuming `PCON` mode - */ - LSPCON::Mode preferredMode {LSPCON::Mode::ProtocolConverter}; - - /** - * The corresponding LSPCON driver; `NULL` if no onboard chip - */ - LSPCON *lspcon {nullptr}; - }; - - /** - * User-defined LSPCON chip info for all possible framebuffers - */ - FramebufferLSPCON lspcons[MaxFramebufferConnectorCount]; - - /// MARK:- Manage user-defined LSPCON chip info for all framebuffers - - /** - * Setup the LSPCON driver for the given framebuffer - * - * @param that The opaque framebuffer controller instance - * @param framebuffer The framebuffer that owns this LSPCON chip - * @param displayPath The corresponding opaque display path instance - * @note This method will update fields in `lspcons` accordingly on success. - */ - static void framebufferSetupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath); - - /** - * [Convenient] Check whether the given framebuffer has an onboard LSPCON chip - * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. - */ - static inline bool framebufferHasLSPCON(uint32_t index) { - return callbackIGFX->lspcons[index].hasLSPCON; - } - - /** - * [Convenient] Check whether the given framebuffer already has LSPCON driver initialized - * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. - */ - static inline bool framebufferHasLSPCONInitialized(uint32_t index) { - return callbackIGFX->lspcons[index].lspcon != nullptr; - } - - /** - * [Convenient] Get the non-null LSPCON driver associated with the given framebuffer - * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return The LSPCON driver instance. - */ - static inline LSPCON *framebufferGetLSPCON(uint32_t index) { - return callbackIGFX->lspcons[index].lspcon; - } - - /** - * [Convenient] Set the non-null LSPCON driver for the given framebuffer - * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer - */ - static inline void framebufferSetLSPCON(uint32_t index, LSPCON *lspcon) { - callbackIGFX->lspcons[index].lspcon = lspcon; - } - - /** - * [Convenient] Get the preferred LSPCON mode for the given framebuffer - * - * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` - * @return The preferred adapter mode. - */ - static inline LSPCON::Mode framebufferLSPCONGetPreferredMode(uint32_t index) { - return callbackIGFX->lspcons[index].preferredMode; - } - - /// MARK:- I2C-over-AUX Transaction APIs - - /** - * [Advanced] Reposition the offset for an I2C-over-AUX access - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset The address of the next register to access - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advSeekI2COverAUX(framebuffer:displayPath:address:offset:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. - */ - static IOReturn advSeekI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint8_t flags); - - /** - * [Advanced] Read from an I2C slave via the AUX channel - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset Address of the first register to read from - * @param length The number of bytes requested to read starting from `offset` - * @param buffer A non-null buffer to store the bytes - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advReadI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. - */ - static IOReturn advReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); - - /** - * [Advanced] Write to an I2C slave via the AUX channel - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param offset Address of the first register to write to - * @param length The number of bytes requested to write starting from `offset` - * @param buffer A non-null buffer containing the bytes to write - * @param flags A flag reserved by Apple. Currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note Method Signature: `AppleIntelFramebufferController::advWriteI2COverAUX(framebuffer:displayPath:address:offset:length:buffer:flags:)` - * @note Built upon Apple's original `ReadI2COverAUX()` and `WriteI2COverAUX()` APIs. - */ - static IOReturn advWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint32_t offset, uint16_t length, uint8_t *buffer, uint8_t flags); - - /** - * [Basic] Read from an I2C slave via the AUX channel - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param length The number of bytes requested to read and must be <= 16 (See below) - * @param buffer A buffer to store the read bytes (See below) - * @param intermediate Set `true` if this is an intermediate read (See below) - * @param flags A flag reserved by Apple; currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in - * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. - * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. - * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. - * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) - * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) - * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) - * @note Method Signature: `AppleIntelFramebufferController::ReadI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:flags:)` - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::ReadI2COverAUX()` method. - * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. - * @ref TODO: Add the link to the blog post. [Working In Progress] - */ - static IOReturn wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags); - - /** - * [Basic] Write to an I2C adapter via the AUX channel - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @param address The 7-bit address of an I2C slave - * @param length The number of bytes requested to write and must be <= 16 (See below) - * @param buffer A buffer that stores the bytes to write (See below) - * @param intermediate Set `true` if this is an intermediate write (See below) - * @param flags A flag reserved by Apple; currently always 0. - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note The number of bytes requested to read cannot be greater than 16, because the burst data size in - * a single AUX transaction is 20 bytes, in which the first 4 bytes are used for the message header. - * @note Passing a `0` buffer length and a `NULL` buffer will start or stop an I2C transaction. - * @note When `intermediate` is `true`, the Middle-of-Transaction (MOT, bit 30 in the header) bit will be set to 1. - * (See Section 2.7.5.1 I2C-over-AUX Request Transaction Command in VESA DisplayPort Specification 1.2) - * @note Similar logic could be found at `intel_dp.c` (Intel Linux Graphics Driver; Linux Kernel) - * static ssize_t intel_dp_aux_transfer(struct drm_dp_aux* aux, struct drm_dp_aux_msg* msg) - * @note Method Signature: `AppleIntelFramebufferController::WriteI2COverAUX(framebuffer:displayPath:address:length:buffer:intermediate:)` - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::WriteI2COverAUX()` method. - * @seealso See the actual implementation extracted from my reverse engineering research for detailed information. - * @ref TODO: Add the link to the blog post. [Working In Progress] - */ - static IOReturn wrapWriteI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate); - - /** - * [Wrapper] Retrieve the DPCD info for a given framebuffer port - * - * @param that The hidden implicit `this` pointer - * @param framebuffer A framebuffer instance - * @param displayPath A display path instance - * @return `kIOReturnSuccess` on success, other values otherwise. - * @note This is a wrapper for Apple's original `AppleIntelFramebufferController::GetDPCDInfo()` method. - * Used to inject code to initialize the driver for the onboard LSPCON chip. - */ - static IOReturn wrapGetDPCDInfo(void *that, IORegistryEntry *framebuffer, void *displayPath); /** * PAVP session callback wrapper used to prevent freezes on incompatible PAVP certificates diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index 44b40b37..dee771d1 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -324,7 +324,7 @@ IOReturn IGFX::LSPCONDriverSupport::wrapGetDPCDInfo(void *that, IORegistryEntry // Call the original method DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Will call the original method."); - IOReturn retVal = callbackIGFX->orgGetDPCDInfo(that, framebuffer, displayPath); + IOReturn retVal = callbackIGFX->modLSPCONDriverSupport.orgGetDPCDInfo(that, framebuffer, displayPath); DBGLOG("igfx", "SC: GetDPCDInfo() DInfo: Returns 0x%llx.", retVal); return retVal; } From d8838ffdc504c5927362414f95628f5c7cd8fe58 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 16:55:46 -0700 Subject: [PATCH 087/102] LSPCON: Code Cleanup. --- WhateverGreen/kern_igfx_lspcon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index dee771d1..9da8ebd6 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -180,7 +180,7 @@ void IGFX::LSPCONDriverSupport::processKernel(KernelPatcher &patcher, DeviceInfo snprintf(name, sizeof(name), "framebuffer-con%lu-preferred-lspcon-mode", index); (void)WIOKit::getOSDataValue(info->videoBuiltin, name, pmode); // Assuming PCON mode if invalid mode value (i.e. > 1) specified by the user - lspcons[index].preferredMode = ::LSPCON::Mode::parse(pmode != 0); // TODO: Remove :: + lspcons[index].preferredMode = LSPCON::Mode::parse(pmode != 0); } } } From 6497f64281911334b4230f17c53b6f369fabda5f Mon Sep 17 00:00:00 2001 From: FireWolf Date: Mon, 21 Sep 2020 17:00:31 -0700 Subject: [PATCH 088/102] IGFX: Update the changelog. --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index a4f62974..e8bd739c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ WhateverGreen Changelog - Added MacKernelSDK with Xcode 12 compatibility - Fixed loading on macOS 10.11 and earlier - Extended the maximum link rate fix: Now probe the rate from DPCD automatically and support Intel ICL platforms. (by @0xFireWolf) +- Fixed an issue that LSPCON driver causes a page fault if the maximum link rate fix is not enabled. (by @0xFireWolf) #### v1.4.2 - Fixed `disable-external-gpu` (`-wegnoegpu`) on some systems From af7014328ba66b449e7c639c0fbe3b0a3d5e9853 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 26 Sep 2020 16:41:16 -0700 Subject: [PATCH 089/102] MLR: Fix an issue that the kext is not properly processed on ICL platforms. --- WhateverGreen/kern_igfx_clock.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 9092cf87..ef9d78a5 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -124,8 +124,8 @@ void IGFX::DPCDMaxLinkRateFix::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto raux = patcher.solveSymbol(index, "__ZN14AppleIntelPort7readAUXEjPvj", address, index); - auto gfbp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, index); + auto raux = patcher.solveSymbol(index, "__ZN14AppleIntelPort7readAUXEjPvj", address, size); + auto gfbp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, size); if (raux && gfbp) { patcher.eraseCoverageInstPrefix(raux); From c0dfe32c981e15368f410f0be2291baff3b45567 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Fri, 16 Oct 2020 23:59:31 -0700 Subject: [PATCH 090/102] MLR: Update the link. --- WhateverGreen/kern_igfx_clock.cpp | 2 +- WhateverGreen/kern_igfx_lspcon.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index ef9d78a5..2b033f19 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -213,7 +213,7 @@ IOReturn IGFX::DPCDMaxLinkRateFix::wrapReadAUX(uint32_t address, void *buffer, u // If you are interested in the story behind this fix, take a look at my blog posts. // Phase 1: https://www.firewolf.science/2018/10/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-compromise-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ // Phase 2: https://www.firewolf.science/2018/11/coffee-lake-intel-uhd-graphics-630-on-macos-mojave-a-nearly-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ - // Phase 3: TODO: ADD LINK + // Phase 3: https://www.firewolf.science/2020/10/coffee-lake-intel-uhd-graphics-630-on-macos-catalina-the-ultimate-solution-to-the-kernel-panic-due-to-division-by-zero-in-the-framebuffer-driver/ // Call the original ReadAUX() function to read from DPCD IOReturn retVal = callbackIGFX->modDPCDMaxLinkRateFix.orgReadAUX(address, buffer, length); diff --git a/WhateverGreen/kern_igfx_lspcon.hpp b/WhateverGreen/kern_igfx_lspcon.hpp index 159c0f45..ec4d8bb9 100644 --- a/WhateverGreen/kern_igfx_lspcon.hpp +++ b/WhateverGreen/kern_igfx_lspcon.hpp @@ -444,7 +444,7 @@ struct FramebufferLSPCON { * @warning If not specified, assuming `PCON` mode is preferred. * @warning If invalid mode value found, assuming `PCON` mode */ - LSPCON::Mode preferredMode ;//{ LSPCON::Mode::Value::ProtocolConverter }; + LSPCON::Mode preferredMode { LSPCON::Mode::Value::ProtocolConverter }; /** * The corresponding LSPCON driver; `NULL` if no onboard chip From 17376da7bfc6c11bccfcb95c5bee6cca2acf6cb2 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 00:03:41 -0700 Subject: [PATCH 091/102] Update the changelog. --- Changelog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index e8bd739c..a44bf96b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,11 +1,13 @@ WhateverGreen Changelog ======================= +#### v1.4.4 +- Extended the maximum link rate fix: Now probe the rate from DPCD automatically and support Intel ICL platforms. (by @0xFireWolf) +- Fixed an issue that LSPCON driver causes a page fault if the maximum link rate fix is not enabled. (by @0xFireWolf) + #### v1.4.3 - Added CFL and CML P630 - Added MacKernelSDK with Xcode 12 compatibility - Fixed loading on macOS 10.11 and earlier -- Extended the maximum link rate fix: Now probe the rate from DPCD automatically and support Intel ICL platforms. (by @0xFireWolf) -- Fixed an issue that LSPCON driver causes a page fault if the maximum link rate fix is not enabled. (by @0xFireWolf) #### v1.4.2 - Fixed `disable-external-gpu` (`-wegnoegpu`) on some systems From 6f1f1c58e56c650702a9d56e794a91eec4592c01 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 00:06:38 -0700 Subject: [PATCH 092/102] DVMT: Code cleanup. --- WhateverGreen/kern_igfx.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index b065091a..e83083e8 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -343,8 +343,6 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { }; auto requiresGraphicsPatches = [this]() { - if (modDVMTCalcFix.requiresPatchingGraphics) - return true; if (pavpDisablePatch) return true; if (forceOpenGL) From a508ad74b9d95d4f03bb9e68bf55e0e3c5aaad7a Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 00:12:25 -0700 Subject: [PATCH 093/102] MLR: Update the manual. --- Manual/FAQ.IntelHD.cn.md | 2 +- Manual/FAQ.IntelHD.en.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Manual/FAQ.IntelHD.cn.md b/Manual/FAQ.IntelHD.cn.md index c0a89b11..92deda28 100644 --- a/Manual/FAQ.IntelHD.cn.md +++ b/Manual/FAQ.IntelHD.cn.md @@ -1740,7 +1740,7 @@ EDID 信息可以通过诸如使用 [Linux](https://unix.stackexchange.com/quest ## 修复笔记本内屏返回错误的最大链路速率值的问题 (Dell XPS 15 9570 等高分屏笔记本) 为核显添加 `enable-dpcd-max-link-rate-fix` 属性或者直接使用 `-igfxmlr` 启动参数以解决系统在点亮内屏时直接崩溃的问题。 从 1.3.7 版本开始,此补丁同时修正从屏幕扩展属性里读取的错误速率值问题以解决在 Dell 灵越 7590 系列等新款笔记本上内核崩溃的问题。 -从 1.4.3 版本开始,如果用户未定义 `dpcd-max-link-rate` 属性的话,此补丁将自动从 DPCD 寻找内屏支持的最大链路速率值。此外此补丁已适配 Ice Lake 平台。 +从 1.4.4 版本开始,如果用户未定义 `dpcd-max-link-rate` 属性的话,此补丁将自动从 DPCD 寻找内屏支持的最大链路速率值。此外此补丁已适配 Ice Lake 平台。 ![](https://github.com/acidanthera/WhateverGreen/blob/master/Manual/Img/dpcd_mlr.png) 另外可使用 `dpcd-max-link-rate` 这个属性来为笔记本内屏指定一个最大链路速率值。 4K 内屏一般使用 `0x14`,1080p 内屏使用 `0x0A` 即可。 diff --git a/Manual/FAQ.IntelHD.en.md b/Manual/FAQ.IntelHD.en.md index 00b38e95..258a1624 100644 --- a/Manual/FAQ.IntelHD.en.md +++ b/Manual/FAQ.IntelHD.en.md @@ -2381,7 +2381,7 @@ Or instead of this property, use the boot-arg `-wegnoegpu` Add the `enable-dpcd-max-link-rate-fix` property to `IGPU`, otherwise a kernel panic would happen due to a division-by-zero. Or instead of this property, use the boot-arg `-igfxmlr`. Starting from v1.3.7, it also fixes the invalid max link rate value read from the extended DPCD buffer. This fixes the kernel panic on new laptops, such as Dell Inspiron 7590 with Sharp display. -Starting from v1.4.3, it probes the maximum link rate value automatically if the property `dpcd-max-link-rate` is not specified, and it now supports Ice Lake platforms. +Starting from v1.4.4, it probes the maximum link rate value automatically if the property `dpcd-max-link-rate` is not specified, and it now supports Ice Lake platforms. ![dpcd_mlr](./Img/dpcd_mlr.png) You could also manually specify a maximum link rate value via the `dpcd-max-link-rate` for the builtin display. Typically use `0x14` for 4K display and `0x0A` for 1080p display. All possible values are `0x06` (RBR), `0x0A` (HBR), `0x14` (HBR2) and `0x1E` (HBR3). If an invalid value is specified or property `dpcd-max-link-rate` is not specified, the driver will probe the maximum link rate from DPCD instead. From b791c3059fca430f269fff0c08c5da947a43fe5d Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 11:37:48 -0700 Subject: [PATCH 094/102] IGFX: Improve the code style. --- WhateverGreen/kern_igfx.cpp | 4 ++-- WhateverGreen/kern_igfx.hpp | 18 ++++++------------ WhateverGreen/kern_igfx_clock.cpp | 16 ++++++++-------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index e83083e8..1d5cc4f4 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -313,8 +313,8 @@ void IGFX::processKernel(KernelPatcher &patcher, DeviceInfo *info) { auto submodulesRequiresFramebufferPatch = false; auto submodulesRequiresGraphicsPatch = false; for (auto submodule : submodules) { - submodulesRequiresFramebufferPatch = submodulesRequiresFramebufferPatch || submodule->requiresPatchingFramebuffer; - submodulesRequiresGraphicsPatch = submodulesRequiresGraphicsPatch || submodule->requiresPatchingGraphics; + submodulesRequiresFramebufferPatch |= submodule->requiresPatchingFramebuffer; + submodulesRequiresGraphicsPatch |= submodule->requiresPatchingGraphics; } // Ideally, we could get rid of these two lambda expressions diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index 2fa68a3a..d8dedb77 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -509,7 +509,6 @@ class IGFX { * A submodule to fix the calculation of DVMT preallocated memory on ICL+ platforms */ class DVMTCalcFix: public PatchSubmodule { - private: /** * The amount of DVMT preallocated memory in bytes set in the BIOS */ @@ -531,7 +530,6 @@ class IGFX { * A submodule to fix the maximum link rate reported by DPCD */ class DPCDMaxLinkRateFix: public PatchSubmodule { - private: /** * User-specified maximum link rate value in the DPCD buffer * @@ -700,7 +698,6 @@ class IGFX { * A submodule to support all valid Core Display Clock frequencies on ICL+ platforms */ class CoreDisplayClockFix: public PatchSubmodule { - private: /** * [ICL+] Original AppleIntelFramebufferController::ReadRegister32 function * @@ -760,7 +757,6 @@ class IGFX { * A submodule to fix the calculation of HDMI dividers to avoid the infinite loop */ class HDMIDividersCalcFix: public PatchSubmodule { - private: /** * Represents the current context of probing dividers for HDMI connections */ @@ -823,11 +819,9 @@ class IGFX { * A submodule to support advanced I2C-over-AUX transactions on SKL, KBL and CFL platforms * * @note LSPCON driver enables this module automatically. This module must be placed after the LSPCON module. - * @note This module is not compatible with ICL platforms yet. - * Not sure if Ice Lake graphics card supports native HDMI 2.0 output. + * @note ICL platform does not require this module as it provides native HDMI 2.0 output. */ class AdvancedI2COverAUXSupport: public PatchSubmodule { - private: /** * Set to true to enable verbose output in I2C-over-AUX transactions */ @@ -996,7 +990,7 @@ class IGFX { * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` * @return `true` if the framebuffer has an onboard LSPCON chip, `false` otherwise. */ - inline bool hasLSPCON(uint32_t index) { + bool hasLSPCON(uint32_t index) { return lspcons[index].hasLSPCON; } @@ -1006,7 +1000,7 @@ class IGFX { * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` * @return `true` if the LSPCON driver has already been initialized for this framebuffer, `false` otherwise. */ - inline bool hasLSPCONInitialized(uint32_t index) { + bool hasLSPCONInitialized(uint32_t index) { return lspcons[index].lspcon != nullptr; } @@ -1016,7 +1010,7 @@ class IGFX { * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` * @return The LSPCON driver instance. */ - inline LSPCON *getLSPCON(uint32_t index) { + LSPCON *getLSPCON(uint32_t index) { return lspcons[index].lspcon; } @@ -1026,7 +1020,7 @@ class IGFX { * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` * @param lspcon A non-null LSPCON driver instance associated with the given framebuffer */ - inline void setLSPCON(uint32_t index, LSPCON *lspcon) { + void setLSPCON(uint32_t index, LSPCON *lspcon) { lspcons[index].lspcon = lspcon; } @@ -1036,7 +1030,7 @@ class IGFX { * @param index A **valid** framebuffer index; Must be less than `MaxFramebufferConnectorCount` * @return The preferred adapter mode. */ - inline LSPCON::Mode getLSPCONPreferredMode(uint32_t index) { + LSPCON::Mode getLSPCONPreferredMode(uint32_t index) { return lspcons[index].preferredMode; } diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 2b033f19..6cf3a60a 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -264,17 +264,17 @@ IOReturn IGFX::DPCDMaxLinkRateFix::orgReadAUX(uint32_t address, void *buffer, ui // ICL+ DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to ICL IMP with Address = 0x%x; Length = %u.", address, length); return orgICLReadAUX(port, address, buffer, length); - } else { - // CFL- - DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x%x; Length = %u.", address, length); - return orgCFLReadAUX(controller, framebuffer, address, length, buffer, displayPath); } + + // CFL- + DBGLOG("igfx", "MLR: [COMM] orgReadAUX() Routed to CFL IMP with Address = 0x%x; Length = %u.", address, length); + return orgCFLReadAUX(controller, framebuffer, address, length, buffer, displayPath); } bool IGFX::DPCDMaxLinkRateFix::getFramebufferIndex(uint32_t &index) { - auto framebuffer = port != nullptr ? orgICLGetFBFromPort(*callbackIGFX->gFramebufferController, port) : this->framebuffer; - DBGLOG("igfx", "MLR: [COMM] GetFBIndex() Port at 0x%llx; Framebuffer at 0x%llx.", port, framebuffer); - return AppleIntelFramebufferExplorer::getIndex(framebuffer, index); + auto fb = port != nullptr ? orgICLGetFBFromPort(*callbackIGFX->gFramebufferController, port) : this->framebuffer; + DBGLOG("igfx", "MLR: [COMM] GetFBIndex() Port at 0x%llx; Framebuffer at 0x%llx.", port, fb); + return AppleIntelFramebufferExplorer::getIndex(fb, index); } uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { @@ -303,7 +303,7 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { // Parse all supported link rates reported by DPCD // The last non-zero entry in the table is the maximum link rate supported by the eDP 1.4 panel uint32_t last = 0; - for (int index = 0; index < arrsize(rates); index += 1) { + for (int index = 0; index < arrsize(rates); index++) { // Guard: Table is terminated by a zero entry if (rates[index] == 0) { DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() End of table."); From 1a4e57dea6bba6732235d88e27820e946940d004 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 12:28:18 -0700 Subject: [PATCH 095/102] I2C: Use `routeMultiple()` to route multiple functions elegantly. --- WhateverGreen/kern_igfx_i2c_aux.cpp | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/WhateverGreen/kern_igfx_i2c_aux.cpp b/WhateverGreen/kern_igfx_i2c_aux.cpp index 1d8ebcb8..bc8a61cd 100644 --- a/WhateverGreen/kern_igfx_i2c_aux.cpp +++ b/WhateverGreen/kern_igfx_i2c_aux.cpp @@ -27,23 +27,24 @@ void IGFX::AdvancedI2COverAUXSupport::processKernel(KernelPatcher &patcher, Devi } void IGFX::AdvancedI2COverAUXSupport::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto roa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", address, size); - auto woa = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", address, size); - - if (roa && woa) { - patcher.eraseCoverageInstPrefix(roa); - patcher.eraseCoverageInstPrefix(woa); - orgReadI2COverAUX = reinterpret_cast(patcher.routeFunction(roa, reinterpret_cast(wrapReadI2COverAUX), true)); - orgWriteI2COverAUX = reinterpret_cast(patcher.routeFunction(woa, reinterpret_cast(wrapWriteI2COverAUX), true)); - if (orgReadI2COverAUX && orgWriteI2COverAUX) { - DBGLOG("igfx", "I2C: Functions have been routed successfully"); - } else { - patcher.clearError(); - SYSLOG("igfx", "I2C: Failed to route functions."); + KernelPatcher::RouteRequest requests[] = { + { + "__ZN31AppleIntelFramebufferController14ReadI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhbh", + wrapReadI2COverAUX, + reinterpret_cast(orgReadI2COverAUX) + }, + { + "__ZN31AppleIntelFramebufferController15WriteI2COverAUXEP21AppleIntelFramebufferP21AppleIntelDisplayPathjtPhb", + wrapWriteI2COverAUX, + reinterpret_cast(orgWriteI2COverAUX) } + }; + + if (patcher.routeMultiple(index, requests, address, size)) { + DBGLOG("igfx", "I2C: Functions have been routed successfully"); } else { - SYSLOG("igfx", "I2C: Failed to find symbols."); patcher.clearError(); + SYSLOG("igfx", "I2C: Failed to route functions."); } } From a0abfe0b74aa2a90e0e9e4769e81bf95abdf7849 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 12:44:28 -0700 Subject: [PATCH 096/102] LSPCON, MLR, CDC: Use `routeMultiple()` to route functions. --- WhateverGreen/kern_igfx_clock.cpp | 73 +++++++++++++----------------- WhateverGreen/kern_igfx_lspcon.cpp | 24 ++++------ 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 6cf3a60a..2dd1c4c1 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -124,39 +124,34 @@ void IGFX::DPCDMaxLinkRateFix::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto raux = patcher.solveSymbol(index, "__ZN14AppleIntelPort7readAUXEjPvj", address, size); - auto gfbp = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, size); - - if (raux && gfbp) { - patcher.eraseCoverageInstPrefix(raux); - orgICLReadAUX = reinterpret_cast(patcher.routeFunction(raux, reinterpret_cast(wrapICLReadAUX), true)); - orgICLGetFBFromPort = reinterpret_cast(gfbp); - if (orgICLReadAUX && orgICLGetFBFromPort) { - DBGLOG("igfx", "MLR: [ICL+] Functions have been routed successfully."); - } else { - SYSLOG("igfx", "MLR: [ICL+] Failed to route functions."); - } + KernelPatcher::RouteRequest request = { + "__ZN14AppleIntelPort7readAUXEjPvj", + wrapICLReadAUX, + reinterpret_cast(orgICLReadAUX) + }; + + orgICLGetFBFromPort = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, size); + + if (patcher.routeMultiple(index, &request, 1, address, size) && orgICLGetFBFromPort) { + DBGLOG("igfx", "MLR: [ICL+] Functions have been routed successfully."); } else { - SYSLOG("igfx", "MLR: [ICL+] Failed to find symbols."); patcher.clearError(); + SYSLOG("igfx", "MLR: [ICL+] Failed to route functions."); } } void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForCFL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto raux = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + KernelPatcher::RouteRequest request = { + "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", + wrapCFLReadAUX, + reinterpret_cast(orgCFLReadAUX) + }; - if (raux) { - patcher.eraseCoverageInstPrefix(raux); - orgCFLReadAUX = reinterpret_cast(patcher.routeFunction(raux, reinterpret_cast(wrapCFLReadAUX), true)); - if (orgCFLReadAUX) { - DBGLOG("igfx", "MLR: [CFL-] Functions have been routed successfully."); - } else { - patcher.clearError(); - SYSLOG("igfx", "MLR: [CFL-] Failed to route functions."); - } + if (patcher.routeMultiple(index, &request, 1, address, size)) { + DBGLOG("igfx", "MLR: [CFL-] Functions have been routed successfully."); } else { - SYSLOG("igfx", "MLR: [CFL-] Failed to find symbols."); patcher.clearError(); + SYSLOG("igfx", "MLR: [CFL-] Failed to route functions."); } } @@ -481,25 +476,21 @@ void IGFX::CoreDisplayClockFix::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto pcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", address, size); - auto dcdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); - auto scdcAddress = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); - - if (pcdcAddress && dcdcAddress && scdcAddress && callbackIGFX->AppleIntelFramebufferController__ReadRegister32) { - patcher.eraseCoverageInstPrefix(pcdcAddress); - orgProbeCDClockFrequency = reinterpret_cast(patcher.routeFunction(pcdcAddress, reinterpret_cast(wrapProbeCDClockFrequency), true)); - orgDisableCDClock = reinterpret_cast(dcdcAddress); - orgSetCDClockFrequency = reinterpret_cast(scdcAddress); - orgIclReadRegister32 = reinterpret_cast(callbackIGFX->AppleIntelFramebufferController__ReadRegister32); - if (orgProbeCDClockFrequency && orgIclReadRegister32 && orgDisableCDClock && orgSetCDClockFrequency) { - DBGLOG("igfx", "CDC: Functions have been routed successfully."); - } else { - patcher.clearError(); - SYSLOG("igfx", "CDC: Failed to route functions."); - } + KernelPatcher::RouteRequest request = { + "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", + wrapProbeCDClockFrequency, + reinterpret_cast(orgProbeCDClockFrequency) + }; + + orgDisableCDClock = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); + orgSetCDClockFrequency = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); + orgIclReadRegister32 = reinterpret_cast(callbackIGFX->AppleIntelFramebufferController__ReadRegister32); + + if (patcher.routeMultiple(index, &request, 1, address, size) && orgDisableCDClock && orgSetCDClockFrequency && orgIclReadRegister32) { + DBGLOG("igfx", "CDC: Functions have been routed successfully."); } else { - SYSLOG("igfx", "CDC: Failed to find symbols."); patcher.clearError(); + SYSLOG("igfx", "CDC: Failed to route functions."); } } diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index 9da8ebd6..5ce02131 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -186,23 +186,19 @@ void IGFX::LSPCONDriverSupport::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::LSPCONDriverSupport::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - auto aux = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); - auto gdi = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", address, size); + KernelPatcher::RouteRequest request = { + "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", + wrapGetDPCDInfo, + reinterpret_cast(orgGetDPCDInfo) + }; - if (aux && gdi) { - patcher.eraseCoverageInstPrefix(aux); - patcher.eraseCoverageInstPrefix(gdi); - orgReadAUX = reinterpret_cast(aux); - orgGetDPCDInfo = reinterpret_cast(patcher.routeFunction(gdi, reinterpret_cast(wrapGetDPCDInfo), true)); - if (orgReadAUX && orgGetDPCDInfo) { - DBGLOG("igfx", "SC: Functions have been routed successfully"); - } else { - patcher.clearError(); - SYSLOG("igfx", "SC: Failed to route functions."); - } + orgReadAUX = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + + if (patcher.routeMultiple(index, &request, 1, address, size) && orgReadAUX) { + DBGLOG("igfx", "SC: Functions have been routed successfully"); } else { - SYSLOG("igfx", "SC: Failed to find symbols."); patcher.clearError(); + SYSLOG("igfx", "SC: Failed to route functions."); } } From 2c32485e43158b669764e505713f0992277a46c6 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 12:47:37 -0700 Subject: [PATCH 097/102] IGFX: PatchSubmodule: Restore the virtual destructor. --- WhateverGreen/kern_igfx.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WhateverGreen/kern_igfx.hpp b/WhateverGreen/kern_igfx.hpp index d8dedb77..7d953c1d 100644 --- a/WhateverGreen/kern_igfx.hpp +++ b/WhateverGreen/kern_igfx.hpp @@ -448,6 +448,11 @@ class IGFX { */ class PatchSubmodule { public: + /** + * Virtual destructor + */ + virtual ~PatchSubmodule() = default; + /** * True if this submodule should be enabled */ From 136d745fa5f3bdc80ca32c436b0ace92cbea4406 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 13:03:10 -0700 Subject: [PATCH 098/102] MLR: Add a guard to ensure that we will extract the largest link rate from the table. --- WhateverGreen/kern_igfx_clock.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 2dd1c4c1..24602672 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -308,9 +308,16 @@ uint32_t IGFX::DPCDMaxLinkRateFix::probeMaxLinkRate() { // Calculate the link rate value // Each element in the table is encoded as a multiple of 200 KHz // The decimal value (e.g. 0x14) is encoded as a multiple of 0.27 GHz (270000 KHz) - last = rates[index] * 200 / 270000; + uint32_t current = rates[index] * 200 / 270000; + DBGLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Table[%d] = %5u; Link Rate = %llu; Decimal Value = 0x%02x.", - index, rates[index], static_cast(rates[index]) * 200 * 1000, last); + index, rates[index], static_cast(rates[index]) * 200 * 1000, current); + + // Guard: Ensure that we are searching for the largest link rate in case of an unsorted table reported by the panel + if (current > last) + last = current; + else + SYSLOG("igfx", "MLR: [COMM] ProbeMaxLinkRate() Warning: Detected an unsorted table. Please report with your kernel log."); } // Ensure that the maximum link rate found in the table is supported by the driver From 64f34e31aae60f6bd12193e2179ca9337d5b5a0c Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 13:40:01 -0700 Subject: [PATCH 099/102] IGFX: Add comments on generic register access and global controller. --- WhateverGreen/kern_igfx.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WhateverGreen/kern_igfx.cpp b/WhateverGreen/kern_igfx.cpp index 1d5cc4f4..431017e1 100644 --- a/WhateverGreen/kern_igfx.cpp +++ b/WhateverGreen/kern_igfx.cpp @@ -436,6 +436,10 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a // Accept Kaby FB and enable backlight patches if On (Auto is irrelevant here). bool bklKabyFb = realFramebuffer == &kextIntelKBLFb && cflBacklightPatch == CoffeeBacklightPatch::On; // Solve ReadRegister32 just once as it is shared + // FIXME: Refactor generic register access as a separate submodule. + // Submodules that request access to these functions must set `PatchSubmodule::requiresGenericRegisterAccess` to `true` + // Also we need to consider the case where multiple submodules want to inject code into these functions. + // At this moment, the backlight fix is the only one that wraps these functions. if (bklCoffeeFb || bklKabyFb || RPSControl.enabled || ForceWakeWorkaround.enabled || modCoreDisplayClockFix.enabled) { AppleIntelFramebufferController__ReadRegister32 = patcher.solveSymbol @@ -450,6 +454,7 @@ bool IGFX::processKext(KernelPatcher &patcher, size_t index, mach_vm_address_t a if (!AppleIntelFramebufferController__WriteRegister32) SYSLOG("igfx", "Failed to find WriteRegister32"); } + // FIXME: Same issue here. if (RPSControl.enabled || ForceWakeWorkaround.enabled || modDPCDMaxLinkRateFix.enabled) gFramebufferController = patcher.solveSymbol(index, "_gController", address, size); if (bklCoffeeFb || bklKabyFb) { From cb7166d36c0f85973c0dece4f8cfa3231dad546c Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 15:37:38 -0700 Subject: [PATCH 100/102] DVMT: Replace the value with the GGC register definition. --- WhateverGreen/kern_igfx_memory.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WhateverGreen/kern_igfx_memory.cpp b/WhateverGreen/kern_igfx_memory.cpp index b7d4d1da..4af9e8d3 100644 --- a/WhateverGreen/kern_igfx_memory.cpp +++ b/WhateverGreen/kern_igfx_memory.cpp @@ -38,8 +38,7 @@ void IGFX::DVMTCalcFix::processKernel(KernelPatcher &patcher, DeviceInfo *info) } // Read the DVMT preallocated memory set in BIOS from the GMCH Graphics Control field at 0x50 (PCI0,2,0) - // TODO: Lilu needs to be updated to define the enumeration case `kIOPCIConfigGraphicsControl` - auto gms = WIOKit::readPCIConfigValue(info->videoBuiltin, /*WIOKit::kIOPCIConfigGraphicsControl*/ 0x50, 0, 16) >> 8; + auto gms = WIOKit::readPCIConfigValue(info->videoBuiltin, WIOKit::kIOPCIConfigGraphicsControl, 0, 16) >> 8; // Disable the fix if the GMS value can be calculated by Apple's formula correctly // Reference: 10th Generation Intel Processor Families: Datasheet, Volume 2, Section 4.1.28 From cd05e0606f15591f20970c574fe1607261ac9885 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sat, 17 Oct 2020 16:11:10 -0700 Subject: [PATCH 101/102] LSPCON, MLR, CDC: Use the new `solveMultiple()` API. --- WhateverGreen/kern_igfx_clock.cpp | 29 +++++++++++++++++++---------- WhateverGreen/kern_igfx_lspcon.cpp | 12 ++++++++---- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 24602672..39b107b4 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -124,15 +124,19 @@ void IGFX::DPCDMaxLinkRateFix::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForICL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - KernelPatcher::RouteRequest request = { + KernelPatcher::RouteRequest routeRequest = { "__ZN14AppleIntelPort7readAUXEjPvj", wrapICLReadAUX, - reinterpret_cast(orgICLReadAUX) + orgICLReadAUX }; - orgICLGetFBFromPort = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", address, size); + KernelPatcher::SolveRequest solveRequest = { + "__ZN31AppleIntelFramebufferController13getFBFromPortEP14AppleIntelPort", + orgICLGetFBFromPort + }; - if (patcher.routeMultiple(index, &request, 1, address, size) && orgICLGetFBFromPort) { + if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && + patcher.solveMultiple(index, &solveRequest, 1, address, size)) { DBGLOG("igfx", "MLR: [ICL+] Functions have been routed successfully."); } else { patcher.clearError(); @@ -144,7 +148,7 @@ void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForCFL(KernelPatcher &patch KernelPatcher::RouteRequest request = { "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", wrapCFLReadAUX, - reinterpret_cast(orgCFLReadAUX) + orgCFLReadAUX }; if (patcher.routeMultiple(index, &request, 1, address, size)) { @@ -483,17 +487,22 @@ void IGFX::CoreDisplayClockFix::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - KernelPatcher::RouteRequest request = { + KernelPatcher::RouteRequest routeRequest = { "__ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv", wrapProbeCDClockFrequency, - reinterpret_cast(orgProbeCDClockFrequency) + orgProbeCDClockFrequency + }; + + KernelPatcher::SolveRequest solveRequests[] = { + {"__ZN31AppleIntelFramebufferController14disableCDClockEv", orgDisableCDClock}, + {"__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", orgSetCDClockFrequency} }; - orgDisableCDClock = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController14disableCDClockEv", address, size); - orgSetCDClockFrequency = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController19setCDClockFrequencyEy", address, size); orgIclReadRegister32 = reinterpret_cast(callbackIGFX->AppleIntelFramebufferController__ReadRegister32); - if (patcher.routeMultiple(index, &request, 1, address, size) && orgDisableCDClock && orgSetCDClockFrequency && orgIclReadRegister32) { + if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && + patcher.solveMultiple(index, solveRequests, address, size) && + orgIclReadRegister32) { DBGLOG("igfx", "CDC: Functions have been routed successfully."); } else { patcher.clearError(); diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index 5ce02131..763d5e13 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -186,15 +186,19 @@ void IGFX::LSPCONDriverSupport::processKernel(KernelPatcher &patcher, DeviceInfo } void IGFX::LSPCONDriverSupport::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { - KernelPatcher::RouteRequest request = { + KernelPatcher::RouteRequest routeRequest = { "__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath", wrapGetDPCDInfo, - reinterpret_cast(orgGetDPCDInfo) + orgGetDPCDInfo }; - orgReadAUX = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", address, size); + KernelPatcher::SolveRequest solveRequest = { + "__ZN31AppleIntelFramebufferController7ReadAUXEP21AppleIntelFramebufferjtPvP21AppleIntelDisplayPath", + orgReadAUX + }; - if (patcher.routeMultiple(index, &request, 1, address, size) && orgReadAUX) { + if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && + patcher.solveMultiple(index, &solveRequest, 1, address, size)) { DBGLOG("igfx", "SC: Functions have been routed successfully"); } else { patcher.clearError(); From c7c91b53e039c14d857c40cc706201d34e1207c8 Mon Sep 17 00:00:00 2001 From: FireWolf Date: Sun, 18 Oct 2020 16:42:30 -0700 Subject: [PATCH 102/102] LSPCON, CDC, MLR, I2C: Remove unnecessary `patcher.clearError()` calls.. --- WhateverGreen/kern_igfx_clock.cpp | 18 ++++++------------ WhateverGreen/kern_igfx_i2c_aux.cpp | 6 ++---- WhateverGreen/kern_igfx_lspcon.cpp | 6 ++---- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/WhateverGreen/kern_igfx_clock.cpp b/WhateverGreen/kern_igfx_clock.cpp index 39b107b4..d3802c27 100644 --- a/WhateverGreen/kern_igfx_clock.cpp +++ b/WhateverGreen/kern_igfx_clock.cpp @@ -136,12 +136,10 @@ void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForICL(KernelPatcher &patch }; if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && - patcher.solveMultiple(index, &solveRequest, 1, address, size)) { + patcher.solveMultiple(index, &solveRequest, 1, address, size)) DBGLOG("igfx", "MLR: [ICL+] Functions have been routed successfully."); - } else { - patcher.clearError(); + else SYSLOG("igfx", "MLR: [ICL+] Failed to route functions."); - } } void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForCFL(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { @@ -151,12 +149,10 @@ void IGFX::DPCDMaxLinkRateFix::processFramebufferKextForCFL(KernelPatcher &patch orgCFLReadAUX }; - if (patcher.routeMultiple(index, &request, 1, address, size)) { + if (patcher.routeMultiple(index, &request, 1, address, size)) DBGLOG("igfx", "MLR: [CFL-] Functions have been routed successfully."); - } else { - patcher.clearError(); + else SYSLOG("igfx", "MLR: [CFL-] Failed to route functions."); - } } void IGFX::DPCDMaxLinkRateFix::processFramebufferKext(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) { @@ -502,12 +498,10 @@ void IGFX::CoreDisplayClockFix::processFramebufferKext(KernelPatcher &patcher, s if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && patcher.solveMultiple(index, solveRequests, address, size) && - orgIclReadRegister32) { + orgIclReadRegister32) DBGLOG("igfx", "CDC: Functions have been routed successfully."); - } else { - patcher.clearError(); + else SYSLOG("igfx", "CDC: Failed to route functions."); - } } void IGFX::CoreDisplayClockFix::sanitizeCDClockFrequency(AppleIntelFramebufferController *that) { diff --git a/WhateverGreen/kern_igfx_i2c_aux.cpp b/WhateverGreen/kern_igfx_i2c_aux.cpp index bc8a61cd..af29f1d9 100644 --- a/WhateverGreen/kern_igfx_i2c_aux.cpp +++ b/WhateverGreen/kern_igfx_i2c_aux.cpp @@ -40,12 +40,10 @@ void IGFX::AdvancedI2COverAUXSupport::processFramebufferKext(KernelPatcher &patc } }; - if (patcher.routeMultiple(index, requests, address, size)) { + if (patcher.routeMultiple(index, requests, address, size)) DBGLOG("igfx", "I2C: Functions have been routed successfully"); - } else { - patcher.clearError(); + else SYSLOG("igfx", "I2C: Failed to route functions."); - } } IOReturn IGFX::AdvancedI2COverAUXSupport::wrapReadI2COverAUX(void *that, IORegistryEntry *framebuffer, void *displayPath, uint32_t address, uint16_t length, uint8_t *buffer, bool intermediate, uint8_t flags) { diff --git a/WhateverGreen/kern_igfx_lspcon.cpp b/WhateverGreen/kern_igfx_lspcon.cpp index 763d5e13..11337a3f 100644 --- a/WhateverGreen/kern_igfx_lspcon.cpp +++ b/WhateverGreen/kern_igfx_lspcon.cpp @@ -198,12 +198,10 @@ void IGFX::LSPCONDriverSupport::processFramebufferKext(KernelPatcher &patcher, s }; if (patcher.routeMultiple(index, &routeRequest, 1, address, size) && - patcher.solveMultiple(index, &solveRequest, 1, address, size)) { + patcher.solveMultiple(index, &solveRequest, 1, address, size)) DBGLOG("igfx", "SC: Functions have been routed successfully"); - } else { - patcher.clearError(); + else SYSLOG("igfx", "SC: Failed to route functions."); - } } void IGFX::LSPCONDriverSupport::setupLSPCON(void *that, IORegistryEntry *framebuffer, void *displayPath) {