Sunday, April 7, 2019

Linux Mint Laptop Brightness Bug

I'm using Linux Mint on a ThinkPad T420s. There is this strange bug whereby the Brightness Up/Down keys sometimes behave erratically. On classic ThinkPads like mine, the key combination is Fn+Home and Fn+End. This problem is at least partly caused by the different algorithms of the power manager of the desktop environment (Cinnamon or MATE) and the laptop firmware. The power manager calculates the steps in linear steps, but the laptop doesn't do that.


This behaviour is different between Linux Mint 19.1 Cinnamon and MATE. It is also different in Ubuntu 18.04.1.

In the Live DVD (thus more or less the default install), we can observe this. In the Live DVD of Linux Mint 19.1 MATE, this problem manifests itself as erratic brightness key behaviour. For example, when we press Brightness Up and then Brightness Down, sometimes the brightness does not go back to the original value. Also, when we dial down the brightness to the minimum and then press Brightness Down again, the screen briefly turns off and then on again.

This problem is not observed in the Live DVDs of Linux Mint 19.1 Cinnamon and Ubuntu 18.04.1. I have not investigated Ubuntu 18.04.1, but in Linux Mint 19.1 Cinnamon, it is because of this default setting:


$ gsettings get org.cinnamon.settings-daemon.plugins.power backlight-helper-preference-order
org.cinnamon.settings-daemon.plugins.power backlight-helper-preference-order ['firmware', 'platform', 'raw']


csd-power reads this setting, and then it calls csd-backlight-helper with those values. For example, with the setting above, csd-power will call csd-backlight-helper like this:

/usr/lib/x86_64-linux-gnu/cinnamon-settings-daemon/csd-backlight-helper --get-brightness -b firmware -b platform -b raw

Upon receiving that command, csd-backlight-helper looks for the brightness file with the type firmware, then if it's not found it looks for a brightness file with the type platform, and if it's also not found then it looks for a brightness file with the type raw. The way it does this is by asking for the list of backlight devices:

csd_backlight_helper_get_best_backlight (gchar** preference_list)
{
        GList *devices;
        GUdevClient *client;
        client = g_udev_client_new (NULL);
        devices = g_udev_client_query_by_subsystem (client, "backlight");
...

And then looking for one that matches the type specified in the command line. And the way it does that, is by querying the type file in the sysfs device directory:

csd_backlight_helper_get_type (GList *devices, const gchar *type)
{
        const gchar *type_tmp;
        GList *d;
        for (d = devices; d != NULL; d = d->next) {
                type_tmp = g_udev_device_get_sysfs_attr (d->data, "type");
...

What it essentially does it, it looks in the directory /sys/class/backlight, then for each subdirectory, it reads the file <subdir>/type, then checks its contents. If it matches the requested type, then proceed. On my laptop, there are two subdirectories (actually, links to other sysfs directories): acpi_video0 and intel_backlight. /sys/class/backlight/acpi_video0/type contains firmware, and /sys/class/backlight/intel_backlight contains raw. Both of these subdirectories contain a brightness file, which can be used to set the brightness. There is also an actual_brightness file, which is read-only, but so far it seems that actual_brightness and brightness always contain the same value. I might be wrong, so please comment if you have more information.
Now, the problem is that both the power manager and the laptop's firmware set the brightness. They really do this twice. And they sometimes have different ideas. If we tell csd-backlight-helper to use the firmware control method, all is fine because on this laptop, the brightness values exposed by the firmware is 0 through 15 and is linear. For example, if the current brightness is 8 and we press the Brightness Up key, then csd-power reads the maximum brightness and current brightness, calculates the next step, and sets the brightness to 9. It then verifies what it's done by reading the maximum brightness and current brightness again, and shows the change with the OSD.
If we use the platform control method, the OSD doesn't show up because on this laptop there is no backlight device with the platform control method.
However, if we use the raw control method, there is a problem. The laptop itself sets the brightness with these values: (the numbers on the left column are the corresponding brightness values obtained via the firmware backlight device)

15  4437
14  3567
13  2871
12  2262
11  1775
10  1392
 9  1096
 8  853
 7  626
 6  470
 5  348
 4  261
 3  191
 2  139
 1  104
 0  70
We can see that the steps are not linear. However, using the raw interface, we can manually set the brightness to any value.
This is what happens to the raw brightness value when we use the raw method:

4437 --> the original, maximum brightness value corresponding to 15 on the firmware's reported brightness

4216 --> the original brightness value minus 221
3567 --> the brightness value set by the laptop itself, corresponding to brightness level 14

3346 --> 3567 minus 221
2871 --> brightness value corresponding to level 13

2650 --> 2871 minus 221
2262 --> brightness value corresponding to level 12

2041 --> 2262 minus 221
1775 --> brightness value corresponding to level 11

1554 --> 1775 minus 221
1392 --> brightness value corresponding to level 10

1171 --> 1392 minus 221
1096 --> brightness value corresponding to level 9

875  --> 1096 minus 221
853  --> brightness value corresponding to level 8

632  --> 853 minus 221
626  --> brightness value corresponding to level 7

405  --> 626 minus 221
470  --> brightness value corresponding to level 6

249  --> 470 minus 221
348  --> brightness value corresponding to level 5, NOTE THAT the brightness actually increased

127
261  --> brightness value corresponding to level 4

40
191  --> brightness value corresponding to level 3

0
139  --> brightness value corresponding to level 2

0
104  --> brightness value corresponding to level 1

0
70   --> brightness value corresponding to level 0

0
70   --> brightness value corresponding to level 0

... and so on. Also note that 4437 = 221 x 20 + 17, and 221 = 17 x 13.
So this is why with the raw method, the brightness jumps around somewhat. This also happens in MATE, because mate-power-backlight-helper always uses the intel_backlight interface. This code snippet is from mate-power-manager/src/gpm-backlight-helper.c:

 /* available kernel interfaces in priority order */
 static const gchar *backlight_interfaces[] = {
  "gmux_backlight",
  "nv_backlight",
  "nvidia_backlight",
  "intel_backlight",
  "dell_backlight",
  "asus_laptop",
  "toshiba",
  "eeepc",
  "eeepc-wmi",
  "thinkpad_screen",
  "acpi_video1",
  "mbp_backlight",
  "acpi_video0",
  "fujitsu-laptop",
  "sony",
  "samsung",
  NULL,
 };

I also found some kind of workaround. In MATE, if we set /sys/module/video/parameters/brightness_switch_enabled to N, the brightness doesn't jump around, but there is a bug that resets the brightness when we plug or when we unplug the ac adapter. The alternative is to chmod /sys/class/backlight/intel_backlight/brightness to 444, so mate-power-backlight-helper can't modify the brightness and it just lets the laptop to set its own brightness, without the need to set /sys/module/video/parameters/brightness_switch_enabled to N.
The laptop does the brightness adjustment by means of ACPI events. In drivers/acpi/acpi_video.c,

static void brightness_switch_event(struct acpi_video_device *video_device,
        u32 event)
{
 if (!brightness_switch_enabled)
  return;

 video_device->switch_brightness_event = event;
 schedule_delayed_work(&video_device->switch_brightness_work, HZ / 10);
}
Looking at that code, it seems that the event being handled with a delay. This means there is a possible race condition with csd-power / gsd-power / mate-power-manager. Also, it seems that the acpi event modifies the brightness after the power manager does, maybe because of the delayed work. Therefore, with the chmod workaround, the OSD still shows up, but it's delayed. When we turn up the brightness to maximum (15 on firmware and 4437 on raw), the OSD still shows an incomplete (not full) brightness bar. This is because the power manager manages to get priority execution and reads the brightness value before the video acpi module changes the value.










No comments: