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
, andACLCutoffAndDelta
.
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:
- Run
gen_panel_prop.py
to generateintel_legacy_panel_data.bin
. Put the generated file in your firmware directory (and if needed, your initramfs). - Patch the sources for the kernel of your choice. Compile and install it.
- 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.