>> Find the nature of the issue

Now the question becomes why the framebuffer driver initializes the field that represents the number of lanes to 0. Since there is not too much information about it, we need to analyze the callers that invoke SetupDPTimings(), and there is only one such function, namely AppleIntelFramebufferController::SetupClocks(AppleIntelFramebuffer*, AppleIntelDisplayPath*, IODetailedTimingInformationV2 const*, AppleIntelFramebufferController::CRTCParams*).

It is a really huge function, but there are several lines before invoking SetupDPTimings() that draw my attention.

//
// Assembly code of AppleIntelCFLGraphicsFramebuffer on macOS 10.12.2 beta 1
//
000000000004219e         movzxb     0x2387(%rbx), %eax      // %eax = framebuffer->field_0x2387
00000000000421a5         movl       %eax, 0x25c0(%rbx)      // framebuffer->field_0x25c0 = %eax
00000000000421ab         movzxb     0x2389(%rbx), %eax      // %eax = framebuffer->field_0x2389
00000000000421b2         movl       %eax, 0x25bc(%rbx)      // framebuffer->field_0x25bc = %eax

Recall that the field at 0x25b in an AppleIntelFramebuffer instance represents the number of lanes, so we can know from the above lines that the value of this field is assigned by another field at 0x2389, and a similar value assignment also happens on the field at 0x25c0.

//
// Assembly code of AppleIntelCFLGraphicsFramebuffer on macOS 10.12.2 beta 1
//
000000000004213a         incq       qword_aa928+53648       // Used for debugging only, ignore this.
0000000000042141         cmpb       $0x0, 0x2387(%rbx)      // if framebuffer->field_0x2387 == 0
0000000000042148         je         loc_4215a               // then goto 0x4215a and read DPCD again

000000000004214a         incq       qword_aa928+53664       // Used for debugging only, ignore this.
0000000000042151         cmpb       $0x0, 0x2389(%rbx)      // if framebuffer->field_0x2389 == 0
0000000000042158         jne        loc_4219e               // then goto 0x4215a and read DPCD again
                                                            // else goto 0x4219e and record the data

loc_4215a:
000000000004215a         incq       qword_aa928+53656       // Used for debugging only, ignore this.
0000000000042161         leaq       aIgfbInvParas, %rdi     // arg0 of kprintf()
// "[IGFB][ERROR  ] Invalid link parameters..re-attempt a DPCD read\\n"
0000000000042168         xorl       %eax, %eax
000000000004216a         call       _kprintf                // Print to the kernel logs
000000000004216f         movq       %r12, %rdi              // %rdi holds the reference to `this`
0000000000042172         call       __ZN31AppleIntelFramebufferController15enableVDDForAuxEv
0000000000042177         movq       %r12, %rdi              // arg0: `this`
000000000004217a         movq       %rbx, %rsi              // arg1: framebuffer instance
000000000004217d         movq       %r15, %rdx              // arg2: displayPath instance
0000000000042180         call      
__ZN31AppleIntelFramebufferController11GetDPCDInfoEP21AppleIntelFramebufferP21AppleIntelDisplayPath
0000000000042185         testl      %eax, %eax              // if retVal == 0
0000000000042187         je         loc_4219e               // (success) then goto 0x4219e 
                                                            // (failure) else DPCD read fails again

0000000000042189         incq       qword_aa928+53672       // Used for debugging only, ignore this.
0000000000042190         leaq       aIgfberrorDpcdR, %rdi   // arg0 of kprintf()                    
// "[IGFB][ERROR  ] DPCD read re-attempt too failed\\n"        
0000000000042197         xorl       %eax, %eax
0000000000042199         call       _kprintf                // Print to the kernel logs

loc_4219e:
000000000004219e         movzxb     0x2387(%rbx), %eax      // %eax = framebuffer->field_0x2387
00000000000421a5         movl       %eax, 0x25c0(%rbx)      // framebuffer->field_0x25c0 = %eax
00000000000421ab         movzxb     0x2389(%rbx), %eax      // %eax = framebuffer->field_0x2389
00000000000421b2         movl       %eax, 0x25bc(%rbx)      // framebuffer->field_0x25bc = %eax

If we scroll up a little bit, we can find that the driver compares the values of the field at 0x2387 and 0x2389 against 0. According to the first parameter value (passed in register %rdi) of the kprintf call at offset 0x4216a, non-zero values indicate that link parameters are valid, and therefore it is needless to perform another DPCD read. Since we have already known that framebuffer->field_0x2389 is 0, we expect to see the line [IGFB][ERROR ] Invalid link parameters..re-attempt a DPCD read\\n is printed in the kernel logs. In addition, the driver also checks the return value of GetDPCDInfo() against 0. If it is not 0, the driver then prints the line [IGFB][ERROR ] DPCD read re-attempt too failed\\n, indicating the failure of a DPCD read. In summary, we now need to dump and examine the kernel log to figure out whether DPCD is read without any error or not.

// Inside `AppleIntelFramebufferController::SetupClocks()`
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][ERROR  ] Invalid link parameters..re-attempt a DPCD read

// Inside `AppleIntelFramebufferController::GetDPCDInfo()`
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][INFO   ] FB0: OUI for display
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][INFO   ]  38 EC 11 0
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][INFO   ]  0 0 0 0
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][INFO   ] FB0: Display port config ver is 1.4

// `AppleIntelFramebufferController::GetDPCDInfo()` returns
// Back to `AppleIntelFramebufferController::SetupClocks()`
kernel: (AppleIntelCFLGraphicsFramebuffer) [IGFB][ERROR  ] DPCD read re-attempt too failed

We can clearly see that DPCD read fails from the above kernel logs. To be more specific, GetDPCDInfo() returned with an error after printing the DisplayPort config version, so apparently somethings went wrong or some checks were not passed after the version report. Therefore, we now need to analyze the function GetDPCDInfo().

Again, this is a huge function that is responsible for reading DPCD from the sink device, verifying the values and saving them into the current framebuffer instance. Since it may not be straightforward to figure out what’s going on in this function by interpreting the assembly block by block, let’s take a look at my partial translated version instead.

//
// Represents the first 16 fields of the receiver capabilities
//
// 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 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 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 maxLaneCount;

    // Maximum Downspread
    UInt8 maxDownspread;

    // Other fields omitted in this struct
    // Detailed information can be found in the specification
    UInt8 others[12];
};
/**
    Read the DisplayPort configuration data and save them into the given framebuffer instance

    @param this The hidden implicit `this` pointer
    @param framebuffer A framebuffer instance
    @param displayPath A display path instance
    @return 0 on success, negative values otherwise.
    @note This is just a small proportion of the complete procedure in assembly.
          Translated and documented by FireWolf @ FireWolf Pl.
          Translated pseudocode is only used for research purpose and illustrating the issue.
 */
int AppleIntelFramebufferController::GetDPCDInfo(AppleIntelFramebufferController* this,
                                                 AppleIntelFramebuffer* framebuffer,
                                                 AppleIntelDisplayPath* displayPath)
{
    // Parameters are passed in registers
    // %r15 = %rdi (arg0) Hidden `this`          // movq        %rdi, %r15
    // %r12 = %rsi (arg1) framebuffer            // movq        %rsi, %r12
    // %r14 = %rdx (arg2) displayPath            // movq        %rdx, %r14

    // Prior knowledge:
    // displayPath->field_0x28 (as an Int32) is not 3.
    // This field (@0x28) probably represents the power state of the sink.
    switch (displayPath->field_0x28)
    {
        case 0x2:
        {
            // Function Prototype:
            //
            // /**
            //      Sets the power state of the sink
            //
            //      @param this The hidden implicit `this` pointer
            //      @param framebuffer A framebuffer instance
            //      @param state The new power state
            //      @param displayPath A display path instance
            //      @return 0 on success, negative values otherwise.
            //      @note This function writes `state` to the register at 0x600 over the AUX channel.
            //  */
            // int AppleIntelFramebufferController::SetDPPowerState(AppleIntelFramebufferController* this,
            //                                                      AppleIntelFramebuffer* framebuffer,
            //                                                      UInt8 state,
            //                                                      AppleIntelDisplayPath* displayPath)
            //

            // This function call sets the state to D0. (Reference: DisplayPort Spec, Address Space 0x600)
            if (AppleIntelFramebufferController::SetDPPowerState(this, framebuffer, 
                                                                 0x1, displayPath) != 0)
            {
                // Negative Case
                return retVal_of_SetDPPowerState;
            }

            break;
        }

        case 0x3:
        {
            // Check the sink power state recorded in the controller instance?
            if ((*(this->field_0x1c30) + 0x58) != 0)
            {
                // Same as 0x2 case
                // Set the power state to D0 and check the return value
            }

            break;
        }

        default: break;            
    }

    // Read the first 16 fields of the receiver capabilities.
    // The address mapping for DPCD starts at 0x0.
    // This local variable resides at [%rbp - 0x48].
    UInt8 dpcd_caps[16];    
    // To be straightforward, I will use a struct to represent it.
    // struct DPCDCap16 dpcd_caps;

    // These fields are accessed over AUX channel, and some of them are read only.
    // Apple uses helper functions to accomplish the field accesses.
    //
    // Prototypes of helper functions:
    //
    // Parameters are inferred from the assembly after carefully examined the caller prologue
    // 
    // /**
    //      Read the DisplayPort configuration data over the AUX channel
    //
    //      @param this The hidden implicit `this` pointer
    //      @param framebuffer A framebuffer instance
    //      @param address A valid DisplayPort address mapped for DPCD
    //      @param length The number of bytes to read starting from the given `address`
    //      @param buffer A non-null buffer to save the data read from DPCD
    //      @param displayPath A display path instance
    //      @return 0 on success, negative values otherwise.
    //  */
    // int AppleIntelFramebufferController::ReadAUX(AppleIntelFramebufferController* this,
    //                                              AppleIntelFramebuffer* framebuffer,
    //                                              UInt32 address,
    //                                              UInt16 length,
    //                                              void* buffer,
    //                                              AppleIntelDisplayPath* displayPath);
    // 
    // /**
    //      Write the DisplayPort configuration data over the AUX channel
    //
    //      @param this The hidden implicit `this` pointer
    //      @param framebuffer A framebuffer instance
    //      @param address A valid DisplayPort address mapped for DPCD
    //      @param length The number of bytes to write starting from the given `address`
    //      @param buffer A non-null buffer that contains the data to write to `address`
    //      @param displayPath A display path instance
    //      @return 0 on success, negative values otherwise.
    //  */
    // int AppleIntelFramebufferController::WriteAUX(AppleIntelFramebufferController* this,
    //                                               AppleIntelFramebuffer* framebuffer,
    //                                               UInt32 address,
    //                                               UInt16 length,
    //                                               void* buffer,
    //                                               AppleIntelDisplayPath* displayPath);

    // In my case, the driver reads the DPCD data successfully.
    if (AppleIntelFramebufferController::ReadAUX(this, framebuffer, 0x0, 16, dpcd_caps, displayPath) != 0)
    {
        // Failed to read fields over the AUX channel.
        return error;
    }

    // This condition is true in my case.
    // So the below fields are zeroed in the framebuffer instance.
    if (displayPath->field_0x28 != 3)
    {
        // Zero out all fields that start from 0x2384 and end at 0x2479
        // bzero(((UInt8*) framebuffer) + 0x2384, 0xfc);
        bzero(framebuffer->field_0x2384, 0xfc);
    }

    // As indicated in the DisplayPort 1.2 specification, 
    // the receiver may have some extended capabilities.
    // So the driver reads the extended DPCD data at 0x2200,
    // and override the original capabilities if necessary.
    UInt8 dpcd_extended_caps[16];

    if (AppleIntelFramebufferController::ReadAUX(this, framebuffer, 0x2200, 16, dpcd_extended_caps) == 0)
    {
        // Check whether overriding is required.
        // Override the capabilities if necessary.

        // In my case, extended capabilities are all zeroes,
        // and overriding is not required.
    }

    // Check the capabilities and save them in the framebuffer instance.
    framebuffer->field_0x2384 = dpcd_caps->revision;        // 0x14 stands for 1.4
    framebuffer->field_0x2385 = dpcd_caps->revision & 0x0F; // 1.[4]
    framebuffer->field_0x2386 = dpcd_caps->revision >> 0x4; // [1].4

    // Before checking the link rate, the driver reads the OUI for the connected display.
    // IEEE OUI is 3 bytes long, followed by 6 bytes representing the identity string of the sink device.
    // For some reasons, Apple only reads the first 8 bytes and then prints them in hexadeciaml value.
    UInt8 oui[8]; 
    AppleIntelFramebufferController::ReadAUX(this, framebuffer, 0x400, 0x8, oui);
    kprintf("[IGFB][INFO   ] FB%d: OUI for display\n", (Int32) framebuffer->field_0x1dc);

    int index = 0;                                  // xorl        %ebx, %ebx
    while (index < 8)                               // cmpq        $0x8, %rbx
    {
        kprintf("[IGFB][INFO   ]  %X %X %X %X \n",  // %rdi holds the string reference
                oui[index + 0],                     // movzbl    -0x48(%rbp,%rbx), %esi
                oui[index + 1],                     // movzbl    -0x47(%rbp,%rbx), %edx
                oui[index + 2],                     // movzbl    -0x46(%rbp,%rbx), %ecx
                oui[index + 3])                     // movzbl    -0x45(%rbp,%rbx), %r8d

        index += 4;                                 // addq        $0x4, %rbx
    }

    kprintf("[IGFB][INFO   ] FB%d: Display port config ver is %d.%d\n",
            (Int32) framebuffer->field_0x1dc,
            framebuffer->field_0x2386 & 0xFF,
            framebuffer->field_0x2385 & 0xFF);

    // %rcx holds the final value of the maximum link rate
    int maxLinkRate = 0;

    // %rax holds the initial value that reads from DPCD
    // %rax = dpcd_caps->maxLinkRate
    switch (dpcd_caps->maxLinkRate)
    {
        case 0x1E:
        {
            // TODO: What is this check?
            // displayPath->field_0x455 is a byte (bit fields)
            if ((displayPath->field_0x455 & 0x8) == 0)
            {
                // Downgrad from 8.1 Gbps (HBR3) to 5.4 Gbps (HBR2)
                maxLinkRate = 0x14;

                // Same check as the case 0x14
                if (*((Int8*) (*(this->field_0x1c30) + 0x58)) < 0)
                {
                    // Downgrade from 5.4 Gbps (HBR2) to 2.7 Gbps (HBR) 
                    maxLinkRate = 0xA;
                }
            }
            else
            {
                maxLinkRate = 0x1E;
            }
        }

        case 0x14:
        {
            maxLinkRate = 0x14;

            // Check the sink power state recorded in the controller instance?
            // I didn't dig into this field; is a negative value possible?
            // In my case, this condition is false, so the link rate remains the same.
            if ((*(this->field_0x1c30) + 0x58) < 0)
            {
                // Downgrade from 5.4 Gbps (HBR2) to 2.7 Gbps (HBR) 
                maxLinkRate = 0xA;
            }
        }

        case 0xA:
        {
            maxLinkRate = 0xA;
        }

        case 0x6:
        {
            maxLinkRate = 0x6;
        }

        default:
        {
            // Use 1.62 Gbps as the fallback value
            maxLinkRate = 0x6;

            // Negative case: 
            // Return from here, leaving fields in the framebuffer instance zeroed.
            return error;
        }
    }

    // Field 0x2387 records the final max link rate value instead of the original one.
    framebuffer->field_0x2387 = maxLinkRate;

    if ((displayPath->field_0x455 & 0x8) != 0)
    {
        // As mentioned above, this condition is false.
        if (displayPath->field_0x28 == 0x3)
        {
            // 0xC is a special value used by Apple internally.
            framebuffer->field_0x2387 = 0xC;
        }
    }

    // Further investigation has shown that the max link rate is not downgraded in my case.
    // (Apple writes 0x14 to the register at 0x100.)

    _kprintf("[IGFB][INFO   ] FB%d Maximum link rate is %#x\n", 
             framebuffer->field_0x1dc, 
             dpcd_caps->maxLinkRate);

    // %rax now holds the value of maximum lane count
    // %rax = dpcd_caps->maxLaneCount
    // Field 0x2389 records the maximum number of lanes
    framebuffer->field_0x2389 = dpcd_caps->maxLaneCount;

    switch (dpcd_caps->maxLaneCount)
    {
        case 0x1:
        case 0x2:
        case 0x4:
            break;

        default:
            // Unsupported/Invalid number of lanes
            return error;
    }

    // The code below just keeps decoding the DPCD data and save
    // them to the corresponding fields in the framebuffer object.

    // ... more code ...
}

Initially, this function checks and adjusts the power state of the sink device accordingly. This part runs without any issues and is not of our interest. However, it is worth knowing that the value of the field at 0x28 in an AppleIntelDisplayPath instance is not 3. This can be checked by placing a “breakpoint” at an appropriate place and dump the register values. Let’s treat this as our prior knowledge, as we will use it later.

Now let’s focus on the local variable dpcd_caps which is a byte array of length 16. Recall from the introduction section that DPCD fields are accessed over the AUX channel, and Apple uses two helper functions, namely ReadAUX() and WriteAUX(), to accomplish the task. According to the DisplayPort specification, the address mapping for DPCD fields starts at 0x0. We are only interested in the first 16 capabilities fields, so we perform a DPCD read from the address 0x0 to 0xF over the AUX channel and tell the helper function to save the values in the given dpcd_caps buffer. ReadAUX() surely returns with no error, otherwise we won’t be able to see the Display port config version line in the kernel log.

Next, all fields in the framebuffer instance that start from 0x2384 and end at 0x2479 are zeroed. In other words, fields at offsets such as 0x2387 and 0x2389 are left zeroed if GetDPCDInfo() returns early than expected. Now we have got the answer, but we still need to figure out which part fails and leads to an error return value. Since we have not seen any output after the line of reporting config version, we can conclude that the verification of the maximum link rate must have failed. In other words, the maximum link rate value reported by DPCD is not one of the supported values, namely 0x06 (RBR), 0x0A (HBR), 0x14 (HBR2) and 0x1E (HBR3). Besides, the final link rate value is saved to the field at 0x2387, and the maximum number of lanes is saved to the field at 0x2389. Further investigation and debugging have shown that the reported value is 0x00 in my case.

Now the mission is completed, and we have found that several fields in a framebuffer instance are zeroed and not recorded at all due to an invalid value of maximum link rate reported by DPCD.

results matching ""

    No results matching ""