Night Light: My first mainline driver
27 October, 2025 - Categories: Linux Mobile - Tags: postmarketOS, Mainlining
I have been daily driving postmarketOS on a OnePlus 6 since March 2024.
I keep a list of the stuff which is not working, which helps me track the progress
of the project through my personal experience.
An entry of this list is: "Night light does not work", this is annoying when
using the phone in the evening as it has a bright OLED screen.
I've been following postmarketOS since 2019, but writing a Linux kernel driver is something I've been hesitant to do, as I perceived the task as the most difficult in the Linux Mobile development; both for lack of documentation on the hardware and doubts on my ability to actually put together the code.
This time something changed, and I've decided to try fixing the problem, even if I was not sure of the result and not very confident in my ability of doing it. I tought: "I will do my best and try harder when I get stuck, worst case i will lose some time".
Well, it took me three days just to understand what was exactly the problem. The effect was "Night Light not available" in GNOME settings, but I did not know what was missing to make it work.
I searched on pmOS issues for something useful and found this comment by Guido, saying:
This is usually an issue of the DRM driver not supporting gamma tables."
But what driver are we talking about? GPU? and what exactly is a gamma table? I really had no clue.
Off to a good start
I convinced myself it was a display panel driver issue (it was not), but after finding nothing useful about "night mode" in the downstream panel driver1, I was back at square one.
On another comment from Robert, I noted some useful keywords: CRTC and GAMMA_LUT. After watching a video training from the pmOS wiki, I had a better idea what I was dealing with: CRTC is short for Cathodic Ray Tube Controller and is a legacy naming for the kernel driver related to the Display Engine (DE), a hardware block responsible for layer mixing and color correction. Later I found that the DE used in recent Qualcomm SoC is called DPU.
By looking at the existing mainline driver, I found this function call.
drm_crtc_enable_color_mgmt(crtc, 0, true, 0);
If we look at the function prototype from the kernel documentation
void drm_crtc_enable_color_mgmt(struct drm_crtc *crtc, uint degamma_lut_size, bool has_ctm, uint gamma_lut_size)
We can see that has_ctm=true and gamma_lut_size=0, so the driver is
reporting no GAMMA_LUT capability.
With great surprise, after changing gamma_lut_size to a non-zero value, the
Night Light page was no longer disabled!
Finding the missing part
Looking at the rest of the dpu_crtc.c, we see
that the driver is setting up only the DSPP PCC block to provide CTM correction,
but no other color calibration block is supported.
grepping on the downstream driver for PCC reveals a useful glossary2
* DSPP sub-blocks
* @SDE_DSPP_IGC DSPP Inverse gamma correction block
* @SDE_DSPP_PCC Panel color correction block
* @SDE_DSPP_GC Gamma correction block
* @SDE_DSPP_HSIC Global HSIC block
* @SDE_DSPP_MEMCOLOR Memory Color block
* @SDE_DSPP_SIXZONE Six zone block
* @SDE_DSPP_GAMUT Gamut bloc
* @SDE_DSPP_DITHER Dither block
* @SDE_DSPP_HIST Histogram block
* @SDE_DSPP_VLUT PA VLUT block
* @SDE_DSPP_AD AD block
* @SDE_DSPP_MAX maximum value
We found the missing part: SDE_DSPP_GC Now we need to figure out how
downstream configures the hardware to write a mainline driver.
The downstream function setting up the GC block can give us a reference on how to write the GAMMA_LUT to the hardware. I wrote the mainline code to write the GAMMA_LUT taking the existing PCC code as reference.
I wrote an initial version of the GC driver... It did nothing.
Adding some printk revealed that my code was never executed because it was
missing a resource allocation step.
I was not able to find any information on the actual format of the GAMMA_LUT, apart from the fact that it used 32-bit registers. I assumed that it was made of 512 8-bit elements for 3 channels (RGB).
It's definitely not the right format 😂
But it's a good sign, it means that the GC block is doing something.
I made some other attempts at guessing the LUT format but only got different combinations of wrong-looking colors.
Guessing is not enough
My format guessing was not leading anywhere. I was trying to sleep when an idea came to mind! What if instead of just writing the LUT, I tried reading it?
[000-007] C0: 027F02BB 01EC0291 032D03C2 01C901EF 03420316 012503FC 01210119 004F0247
[000-007] C1: 00AB0033 001B031A 02CF00FC 004D0238 00240272 01B40318 03BF03BC 00E402D8
[000-007] C2: 023F028C 030D02C4 005F00B4 01A803A0 01650019 03CB018F 006B02E4 03B00201
[008-015] C0: 01F80304 02270380 01A9029C 023503DA 011002E1 027E03D1 01B003A4 03F103E2
[008-015] C1: 036103AE 02BE0388 03DD0311 038D032C 00B40078 037E0175 002C009E 03EF0100
[008-015] C2: 00B40120 03D800E8 03CC0327 001E03B9 01800234 0003007C 033401E4 003002DC
[016-023] C0: 03040393 03E200EC 019700EA 029E020F 034101B4 02BE03F6 02500092 0318033C
[016-023] C1: 022A012D 02B700A2 02AE03BF 03E003B8 0318024E 02A00384 023E015A 01B903D0
It looks a bit random, but looking closely you can see that characters 0 and 7 are always zero, for example:
027F02BB 01EC0291
Picking a value with high hex digits and using a programming calculator shows this:

This looks really much like two 10-bit values packed in a 32-bit register.
If we have two values per register, and we have 512 registers, the size of our GAMMA_LUT is actually 1024 entries, packed in 512 registers.
Adjusting the code for this shows correct colors! 🎉
But wait, is this a green-shift?
That's a simple fix: I assumed RGB format, but apparently it's GBR.
Upstreaming our code
I opened a MR to get some feedback from the postmarketOS kernel developers, and after I addressed the suggestions, I submitted the patch to the LKML to start the integration process into the mainline kernel. There is a nice guide from Linaro.
Getting patches integrated into the mainline kernel is not easy due to the stringent reviews, but it's very important as it's the only way to make sure our code gets carried on with the mainline kernel.
If the patch was accepted as mr to sdm845-mainline but never sent upstream, it would only add up to the hundreds of patches that need to be rebased on every kernel release.
What I learned
Sometimes we avoid doing something because it seems too hard. In reality we underestimate what we can do in a longer span of time (it took me one month of spare time), learning new things as we go.
So go ahead and try to do what you were avoiding, even if it's more difficult than what you can normally manage.