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:
+47
-14
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user