Someone has to do it: Reverse engineering Intel's OLED panel driver

2025-04-28
#computer-repairing #garbage #random-xp

Finally, I managed to put my non-existent reverse engineering skills to good use for once.

I recently upgraded my ThinkPad X1 Yoga 1st gen to an OLED panel. And I was actually stunned to find out that a) Support for proper PWM OLED brightness control for this laptop, which is almost a decade old at this point, is still completely absent in the Linux kernel, and b) Nobody has bothered to do anything about it yet. People have been recommending solutions like xrandr --brightness or icc-brightness, which are both software color filters and will reduce the effective color depth of the display. I have tried out the first solution, and the color banding it causes on lower brightness settings drives me absolutely nuts.

I guess some one has to do it, and that person happens to be me this time. Challenge accepted.

Before someone cries out “but reverse engineering is illegal”, I’ll say this: Yes it is. But I don’t believe that it should be.

So let’s get started!

Obtaining the Windows driver

The official Windows driver can be easily obtained from Lenovo under the entry “Intel OLED Panel Driver for Windows 10 (64-bit) - ThinkPad X1 Yoga (Machine types: 20FQ, 20FR)”. The file name is n1fdl04w.exe. This is just a simple installer that extracts the driver files to C:\DRIVERS and wine runs it without any issue.[1]

When I looked through the driver files, I found all the usual suspects: a Windows kernel driver binary, a .inf file, a file containing the code signing information, and … a .pdb file? Is that what I think it is? Turns out that it is precisely what I thought it was: Lenovo somehow published the debugging symbols in this driver package. This immediately lowered the challenge level from intermediate to low or even trivial. But this also makes me even more baffled: why did nobody take on this task when it’s actually this easy?

First look at the decompiled code

My go-to reverse engineering tool was Cutter. I chose it because it’s small, has a proper native user interface, and has seemingly good Ghidra integration. It was in fact the only reverse engineering tool when I was taking a class on software security. However when I loaded the driver in Cutter, I found the result to be very underwhelming: notably its Ghidra integration failed to apply any data structure information to the decompiled code.

So I decided to give the full Java monstrosity a try. Without a doubt it worked much better. The decompiled code is already pretty close to readable, what’s left to me is to simply name a bunch of local variables, and rewrite the whole thing.

It seems that the Windows driver is communicating with the panel using private APIs provided by the Intel graphics driver, obtained from a call to ExCreateCallback with ObjectName being \CallBack\IGD_PanelDriverShareObj. The communications seem to happen with a function called AuxOrI2cOverAuxAccess, which from its name and usage pattern appears to do something very similar to the drm_dp_dpcd_read / drm_dp_dpcd_write family of functions in the DRM infrastructure of the Linux kernel, which happens to be used by the brightness control logic for newer OLED panels connected to an Intel GPU in the i915 driver as well.

I compared how the hardware is accessed between the Windows driver and the existing OLED brightness control code in the Linux kernel (intel_dp_aux_backlight.c), with the hope that they could share some similarities. It quickly became very obvious to me that they are in fact, extremely different. The Windows driver has lots and lots of hard-coded constants inside the driver, as well as a bunch of nested loops, both of which are absent from the existing driver inside the Linux kernel. It also reads a variety of information from the Windows registry, which is set by the .inf file when the driver is installed. Basically the only commonality that I was able to find between these two drivers is that both contain references to the term “tcon”[2]. That’s it.

Right in the middle of the reverse engineering process, I decided to find out whether there are other laptops that have used a similar driver. I found the X1 Yoga 2nd gen, which wasn’t really surprising at all because it’s using the same panel. But notably that driver doesn’t come with debug symbols. I also found the Alienware 13 R3, which has an extremely similar driver in terms of overall structure. One significant difference is that it does not have the biggest chunk of hard-coded magic numbers found in the Lenovo driver. It instead reads these values from the registry, which are also set by the driver’s .inf file. HP also provides a download to a very similar driver for an alleged model of Spectre x360. However I was unable to find information on a OLED version of this model from that era. The driver itself is practically the same as Dell’s driver.

Developing the reverse engineered driver

I do not have much experience in kernel development at all. I really had no idea where to put this reverse engineered driver. But after surveying the kernel source code surrounding the stuff that I might use, I realized that the code may have to sit in the i915 driver, the primary reason being I couldn’t find a way to get hold of the DP AUX communication channel used by drm_dp_dpcd_* outside of it. I decided to develop the prototype code within the i915 driver first. If later I was told there’s a way to make it a standalone module, I should be able to do it still.

The Windows driver starts nearly all communications with the panel with a handshake. I decided to implement the handshake first so that I can confirm that drm_dp_dpcd_* is the correct family of functions that I should use. Soon enough I got the confirmation that I wanted.

The driver then requests a bunch of data from the panel. This was also decently easy to implement. In the Windows driver these values would be written to the registry so that they don’t have to be retrieved from the hardware again, which can be used to verify the correctness of my driver. But I have actually just removed the Windows installation from this machine a few weeks ago, which wasn’t booted into since I restored it in 2023, so I skipped that step for now.

Then there just two functions left to implement. A function that calculates interpolated brightness values, and the actual process to set the brightness. A lot inside the driver seems to imply that they only have 33 distinct brightness levels ([0,32]) which is later interpolated to the range of [0,100]. The function that performs this interpolation is gigantic, and contains an immense amount of repetitive logic, with only the numbers changing all the time. These numbers for the interpolation procedure are also different in Dell’s driver. The function that sets the brightness is also huge, but it’s because it starts with two big arrays filled with magic numbers. And as noted above, in other vendors’ implementation of this driver, these values are read from the registry. Once I got over that, the rest of this function is actually not very difficult at all.

So at this point I have all the critical functions for setting the brightness implemented. But I’m still in a very early prototyping stage: every bit of code I wrote was piggybacking on the existing dpcd backlight code, and everything is only run once when backlight control is being initialized. Anyway as a first test of actual functionality I tried to set the brightness to 30% as soon as the backlight control initializes.

Nothing happened. The screen is still at full brightness. Devastated, I dug up an external disk and put a new installation of Windows 10 on it in order to compare the values I read from the hardware and the Windows driver read from the hardware.

Unsurprisingly, every single one of them was wrong. I went back to the code, comparing the magic numbers in my code to those in the decompiled code one by one … and found that I had 0xa in one position where it should have been '\a'. D’oh!

Once that has been fixed, the values read by my driver were now correct[3]. But the driver still did nothing. In fact I even checked all the values that have been written into the DPCD registers, and they all appeared to be correct when compared against the simulated result from the Windows driver.

At this point I truly had no idea what I could have done wrong. So I decided to plug all the functions into their proper backlight control interface anyway and try my luck, and lo and behold: it worked instantly. You cannot not imagine my surprise when I saw the screen dimming down when lightsd kicked in to automatically turn the brightness down when the system is booting.

So yes, that was when I got the first working prototype of the driver. Keep in mind that I have no idea what the hardware protocol used is. I don’t know what any of the addresses the driver is reading from / writing to does. I have my own guesses, but I have no idea whether they are correct or not.

With the intention of reducing the amount of magic numbers inside the code, I extracted them into a firmware blob. I also extracted the values used in the interpolation process into the firmware, leaving just one instance of the repetitive interpolation logic in the code. Once I have also removed all the static global variables, I declared the driver finished.

There’s one notable weirdness: when the brightness is set to lower levels, the screen brightness sometimes fluctuates randomly. This behavior is also observed on Windows with the official driver. Since my machine is modded to use the OLED panel, it is not yet known to me that whether this is a hardware issue or a software issue.

If you want to use this driver

Will this work for my panel?

If your computer has an OLED screen and meets one of the following criteria, there’s a chance that this driver might work for you:

  • Your computer is released between 2015-2018, has an Intel processor and uses its integrated GPU.
  • The .inf file in the Windows driver package for your computer’s OLED panel contains the following text: AOR, DELVSS, BrightLevelForACL, and ACLCutoffAndDelta.

Using the driver

The patch and required tools can be obtained here:

https://cgit.chrisoft.org/linux-legacy-oled-brightness.git/

Instructions for ThinkPad X1 Yoga 1st gen / 2nd gen:

  1. Run gen_panel_prop.py to generate intel_legacy_panel_data.bin. Put the generated file in your firmware directory (and if needed, your initramfs).
  2. Patch the sources for the kernel of your choice. Compile and install it.
  3. Boot your new kernel with an additional kernel parameter i915.enable_dpcd_backlight=99. Check if it works.

If you have a different laptop, I would not recommend using the firmware for the ThinkPad panel. You can still try it, but I don’t know what will happen. Your computer may explode. To properly make use of this driver you’ll have to modify gen_panel_prop.py according to the values inside the Windows driver for you panel. Properly filling out the values requires some reverse engineering. I may add more detailed instructions here or in the repo later.

That’s it?

Uhh, so that’s the first time I actually used my reverse engineering skills for something useful. Was it hard? No, not really. Will I do it to myself again? No, but knowing myself I’m positive that I absolutely will do it to myself again some time in the future. Will it ever be upstreamed? You tell me! If you want to see this abomination sent to the upstream, please let me know by contacting me using one of the methods listed here.

Timeline of the project

  • 2025-04-18: Looking through the debugging symbols just for fun.
  • 2025-04-19: Actually started walking through the decompiled code and naming things.
  • 2025-04-20: Started the prototype driver. Successful handshake with the hardware.
  • 2025-04-21: Looking through the same driver from a few other vendors. Finished implementing 3 of 5 critical functions.
  • 2025-04-22: Biggest function reverse engineered. Output values verified against the Windows driver.
  • 2025-04-23: Finished implementation of all required functions. First prototype that has working brightness adjustment.
  • 2025-04-24: Parameterized a lot of the code. Magic numbers moved into firmware files.
  • 2025-04-25: Global static variables removed from the prototype. Driver declared complete.
  • Now: awaiting a lawsuit from Lenovo or Intel.


[1]: I deleted the Windows installation on this computer a couple of weeks ago in order to reclaim some disk space. This would come back to bite me a little bit later…
[2]: “Timing controller”? Could make sense because it’s controlling the brightness with a PWM signal…
[3]: Actually there was an additional detour of one row in the interpolated values being wrong. It was again just a single wrong number that costed me a mere hour to debug.