fix(brightness): use actual_brightness and prefer internal panel device

Switch from reading `brightness` (software-requested) to `actual_brightness`
(hardware-reported), which is the correct file on AMD GPU backlights where
the two values can diverge.

Replace the first-found device selection with a scored preference so that
when multiple backlight devices are present (e.g. amdgpu_bl2 and nvidia_0)
the internal laptop panel is chosen rather than a dGPU output that may
report a fixed ceiling value. Priority: amdgpu/intel (3) > acpi/platform
(2) > unknown (1) > nvidia (0).
This commit is contained in:
2026-04-10 14:48:47 -04:00
parent d00c4aa371
commit aadb0a4a57
+47 -14
View File
@@ -3,19 +3,21 @@
//! Screen brightness monitor. //! Screen brightness monitor.
//! //!
//! Reads from `/sys/class/backlight/<device>/brightness` and //! Reads from `/sys/class/backlight/<device>/actual_brightness` and
//! `/sys/class/backlight/<device>/max_brightness`, polling every 2 seconds //! `/sys/class/backlight/<device>/max_brightness`, polling every 2 seconds
//! and sending `CMD_BRIGHTNESS` packets to all keyboards when the value //! and sending `CMD_BRIGHTNESS` packets to all keyboards when the value
//! changes. //! changes.
//! //!
//! On a KDE6 / Wayland desktop the backlight sysfs path is typically one of: //! `actual_brightness` is preferred over `brightness` because it reflects the
//! - `/sys/class/backlight/intel_backlight` //! real hardware state; on AMD GPU panels these can diverge.
//! - `/sys/class/backlight/amdgpu_bl0` //!
//! - `/sys/class/backlight/acpi_video0` //! When multiple backlight devices are present (e.g. `amdgpu_bl2` and
//! `nvidia_0`), the device most likely to represent the internal laptop panel
//! is selected automatically (amdgpu/intel > acpi/platform > other > nvidia).
//! //!
//! # Future improvement //! # Future improvement
//! Replace polling with an inotify watch on the `brightness` file so changes //! Replace polling with an inotify watch on the `actual_brightness` file so
//! are reflected immediately. //! changes are reflected immediately.
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -65,23 +67,54 @@ pub fn monitor(keyboards: Vec<Keyboard>) {
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Return the sysfs path to the first available backlight device. /// Preference score for a backlight device name (higher = more preferred).
///
/// Internal GPU panels (amdgpu, intel) are preferred over discrete GPU
/// outputs (nvidia), which often report a fixed ceiling value rather than the
/// actual adjustable panel brightness.
fn backlight_score(name: &str) -> u8 {
if name.starts_with("amdgpu_") || name.starts_with("intel_") {
3
} else if name.starts_with("acpi_") || name.starts_with("platform_") {
2
} else if name.starts_with("nvidia_") {
0
} else {
1
}
}
/// Return the sysfs path to the best available backlight device.
///
/// Scores each candidate by name prefix and returns the highest-scoring one.
/// Requires both `actual_brightness` and `max_brightness` to be present.
fn find_backlight() -> Option<PathBuf> { fn find_backlight() -> Option<PathBuf> {
let read_dir = fs::read_dir("/sys/class/backlight").ok()?; let read_dir = fs::read_dir("/sys/class/backlight").ok()?;
let mut best: Option<(u8, PathBuf)> = None;
for entry in read_dir.flatten() { for entry in read_dir.flatten() {
let path = entry.path(); let path = entry.path();
// Verify the required files exist before committing to this device. if !path.join("actual_brightness").exists() || !path.join("max_brightness").exists() {
if path.join("brightness").exists() && path.join("max_brightness").exists() { continue;
return Some(path); }
let name = path.file_name()?.to_string_lossy().into_owned();
let score = backlight_score(&name);
if best.as_ref().map_or(true, |(best_score, _)| score > *best_score) {
best = Some((score, path));
} }
} }
None
best.map(|(_, path)| path)
} }
/// Read the current brightness as a percentage (0-100) from a sysfs backlight /// Read the current brightness as a percentage (0-100) from a sysfs backlight
/// directory, or `None` if the files cannot be read / parsed. /// directory, or `None` if the files cannot be read or parsed.
///
/// Uses `actual_brightness` (hardware-reported) rather than `brightness`
/// (software-requested) for accuracy on AMD GPU panels.
fn read_brightness(backlight: &Path) -> Option<u8> { fn read_brightness(backlight: &Path) -> Option<u8> {
let current: u64 = fs::read_to_string(backlight.join("brightness")) let current: u64 = fs::read_to_string(backlight.join("actual_brightness"))
.ok()? .ok()?
.trim() .trim()
.parse() .parse()