Skip to content

Commit

Permalink
[Feature] Implement new manual predictor (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
salman-farooq-sh authored Dec 25, 2024
1 parent 7bc5c4d commit fc2fc47
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 148 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,43 @@ The `capturer` field will determine how screen contents will be captured. Curren

_Tip:_ run `wluma` with `RUST_LOG=debug` and `capturer="wayland"` to see which protocols are supported by your Wayland compositor, and which one `wluma` chooses to use.

#### Algorithm

The default algorithm that `wluma` uses is called `adaptive`, which is when it learns from you as you continue adjusting brightness manually. It will eventually figure out patterns in how you tend to adjust brightness in dark and lit conditions and depending on what is currently being displayed on the screen, and will beging to do it automatically for you.

If you instead want to preserve control over absolute brightness value, but let `wluma` only do relative adjustments, there is an alternative algorithm called `manual`. It can be useful if you feel like `wluma` is unable to learn the patterns, for example because you don't have a real ambient light sensor, and neither of the alternative ALS inputs are able to capture the real light conditions precisely enough.

Here's how you enable the manual algorithm in the config:

```toml
[als.time]
thresholds = { 0 = "night", 8 = "day", 18 = "night" }

[[output.backlight]]
name = "eDP-1"
path = "/sys/class/backlight/intel_backlight"
capturer = "wayland"
[output.backlight.predictor.manual]
thresholds.day = { 0 = 0, 100 = 10 }
thresholds.night = { 0 = 0, 100 = 60 }
```

In other words, you activate the predictor for a given `output` using `[output.backlight.predictor.manual]`, and then you define thresholds for each ALS condition using the following syntax:

```
thresholds.<als threshold name> = {<luma> = <brightness reduction percentage>}
```

- `luma` is the "whiteness" of your screen contents, measured in percentage, from `0` to `100`.
- Current screen brightness (that you set manually) will be reduced by the corresponding `brightness reduction percentage` based on what is currently being displayed on the screen.
- `als threshold name` is the custom name that you define in ALS thresholds. When using `[als.none]`, the `als threshold name` is `none`.
- You can define as many entries within each threshold as you want (up to 100, for every single `luma` value). The algorithm will interpolate between the values you define.

The example config above expresses the following intention:

- During the day, the screen brightness will be reduced upmost by 10% of the value you set - fully black screen does not reduce the brightness at all, fully white screen reduces it by 10%, screen contents with "whiteness" of 70% will reduce the brightness by 7%, etc.
- During the day, the screen brightness will be reduced upmost by 60% of the value you set - using the same logic as above.

## Run

To run the app, simply launch `wluma` or use the provided systemd user service.
Expand Down
10 changes: 10 additions & 0 deletions src/config/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,29 @@ pub enum Als {
None,
}

#[derive(Debug, Clone)]
pub enum Predictor {
Adaptive,
Manual {
thresholds: HashMap<String, HashMap<u8, u64>>,
},
}

#[derive(Debug, Clone)]
pub struct BacklightOutput {
pub name: String,
pub path: String,
pub capturer: Capturer,
pub min_brightness: u64,
pub predictor: Predictor,
}

#[derive(Debug, Clone)]
pub struct DdcUtilOutput {
pub name: String,
pub capturer: Capturer,
pub min_brightness: u64,
pub predictor: Predictor,
}

#[derive(Debug, Clone)]
Expand Down
19 changes: 16 additions & 3 deletions src/config/file.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Default)]
pub enum Capturer {
#[serde(rename = "wlroots")]
Wlroots,
#[default]
#[serde(rename = "wayland")]
Wayland,
#[serde(rename = "wlr-export-dmabuf-unstable-v1")]
Expand Down Expand Up @@ -41,17 +42,29 @@ pub struct OutputByType {
pub ddcutil: Vec<DdcUtilOutput>,
}

#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "lowercase")]
pub enum Predictor {
#[default]
Adaptive,
Manual {
thresholds: HashMap<String, HashMap<String, u64>>,
},
}

#[derive(Deserialize, Debug)]
pub struct BacklightOutput {
pub name: String,
pub path: String,
pub capturer: Capturer,
pub capturer: Option<Capturer>,
pub predictor: Option<Predictor>,
}

#[derive(Deserialize, Debug)]
pub struct DdcUtilOutput {
pub name: String,
pub capturer: Capturer,
pub capturer: Option<Capturer>,
pub predictor: Option<Predictor>,
}

#[derive(Deserialize, Debug)]
Expand Down
88 changes: 46 additions & 42 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,47 @@ pub fn load() -> Result<app::Config, Box<dyn Error>> {
validate(parse()?)
}

fn match_predictor(predictor: file::Predictor) -> app::Predictor {
match predictor {
file::Predictor::Adaptive => app::Predictor::Adaptive,
file::Predictor::Manual { thresholds } => app::Predictor::Manual {
thresholds: thresholds
.into_iter()
.map(|(k, v)| {
(
k,
v.into_iter()
.map(|(k, v)| (k.parse::<u8>().unwrap(), v))
.collect(),
)
})
.collect(),
},
}
}

fn match_capturer(capturer: file::Capturer) -> app::Capturer {
match capturer {
file::Capturer::None => app::Capturer::None,
file::Capturer::Wlroots => {
log::warn!(
"Config value capturer=\"wlroots\" is deprecated, use capturer=\"wayland\" instead"
);
app::Capturer::Wayland(app::WaylandProtocol::Any)
}
file::Capturer::Wayland => app::Capturer::Wayland(app::WaylandProtocol::Any),
file::Capturer::ExtImageCopyCaptureV1 => {
app::Capturer::Wayland(app::WaylandProtocol::ExtImageCopyCaptureV1)
}
file::Capturer::WlrScreencopyUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrScreencopyUnstableV1)
}
file::Capturer::WlrExportDmabufUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrExportDmabufUnstableV1)
}
}
}

fn parse() -> Result<app::Config, toml::de::Error> {
let file_config = xdg::BaseDirectories::with_prefix("wluma")
.ok()
Expand All @@ -33,54 +74,16 @@ fn parse() -> Result<app::Config, toml::de::Error> {
name: o.name,
path: o.path,
min_brightness: 1,
capturer: match o.capturer {
file::Capturer::None => app::Capturer::None,
file::Capturer::Wlroots => {
log::warn!(
"Config value capturer=\"wlroots\" is deprecated, use capturer=\"wayland\" instead"
);
app::Capturer::Wayland(app::WaylandProtocol::Any)
}
file::Capturer::Wayland => {
app::Capturer::Wayland(app::WaylandProtocol::Any)
}
file::Capturer::ExtImageCopyCaptureV1 => {
app::Capturer::Wayland(app::WaylandProtocol::ExtImageCopyCaptureV1)
}
file::Capturer::WlrScreencopyUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrScreencopyUnstableV1)
}
file::Capturer::WlrExportDmabufUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrExportDmabufUnstableV1)
}
},
capturer: match_capturer(o.capturer.unwrap_or_default()),
predictor: match_predictor(o.predictor.unwrap_or_default()),
})
})
.chain(file_config.output.ddcutil.into_iter().map(|o| {
app::Output::DdcUtil(app::DdcUtilOutput {
name: o.name,
min_brightness: 1,
capturer: match o.capturer {
file::Capturer::None => app::Capturer::None,
file::Capturer::Wlroots => {
log::warn!(
"Config value capturer=\"wlroots\" is deprecated, use capturer=\"wayland\" instead"
);
app::Capturer::Wayland(app::WaylandProtocol::Any)
}
file::Capturer::Wayland => {
app::Capturer::Wayland(app::WaylandProtocol::Any)
}
file::Capturer::ExtImageCopyCaptureV1 => {
app::Capturer::Wayland(app::WaylandProtocol::ExtImageCopyCaptureV1)
}
file::Capturer::WlrScreencopyUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrScreencopyUnstableV1)
}
file::Capturer::WlrExportDmabufUnstableV1 => {
app::Capturer::Wayland(app::WaylandProtocol::WlrExportDmabufUnstableV1)
}
},
capturer: match_capturer(o.capturer.unwrap_or_default()),
predictor: match_predictor(o.predictor.unwrap_or_default()),
})
}))
.chain(file_config.keyboard.into_iter().map(|k| {
Expand All @@ -89,6 +92,7 @@ fn parse() -> Result<app::Config, toml::de::Error> {
path: k.path,
min_brightness: 0,
capturer: Capturer::None,
predictor: app::Predictor::Adaptive,
})
}))
.collect(),
Expand Down
4 changes: 1 addition & 3 deletions src/frame/capturer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use crate::predictor::Controller;

pub mod none;
pub mod wayland;

pub trait Capturer {
fn run(&mut self, output_name: &str, controller: Controller);
fn run(&mut self, output_name: &str, controller: Box<dyn crate::predictor::Controller>);
}
3 changes: 1 addition & 2 deletions src/frame/capturer/none.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use crate::predictor::Controller;
use std::{thread, time::Duration};

#[derive(Default)]
pub struct Capturer {}

impl super::Capturer for Capturer {
fn run(&mut self, _output_name: &str, mut controller: Controller) {
fn run(&mut self, _output_name: &str, mut controller: Box<dyn crate::predictor::Controller>) {
loop {
controller.adjust(0);
thread::sleep(Duration::from_millis(200));
Expand Down
4 changes: 2 additions & 2 deletions src/frame/capturer/wayland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub struct Capturer {
output: Option<WlOutput>,
output_global_id: Option<u32>,
pending_frame: Option<Object>,
controller: Option<Controller>,
controller: Option<Box<dyn Controller>>,
// linux-dmabuf-v1
dmabuf: Option<ZwpLinuxDmabufV1>,
wl_buffer: Option<WlBuffer>,
Expand Down Expand Up @@ -84,7 +84,7 @@ impl Capturer {
}

impl super::Capturer for Capturer {
fn run(&mut self, output_name: &str, controller: Controller) {
fn run(&mut self, output_name: &str, controller: Box<dyn Controller>) {
let connection =
Connection::connect_to_env().expect("Unable to connect to Wayland display");
let display = connection.display();
Expand Down
39 changes: 29 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ fn main() {
.output
.iter()
.filter_map(|output| {
let output = output.clone();
let output_clone = output.clone();

let (als_tx, als_rx) = mpsc::channel();
let (user_tx, user_rx) = mpsc::channel();
let (prediction_tx, prediction_rx) = mpsc::channel();

let (output_name, output_capturer) = match output.clone() {
let (output_name, output_capturer) = match output_clone.clone() {
config::Output::Backlight(cfg) => (cfg.name, cfg.capturer),
config::Output::DdcUtil(cfg) => (cfg.name, cfg.capturer),
};
Expand All @@ -63,6 +63,10 @@ fn main() {
})
.unwrap_or_else(|_| panic!("Unable to start thread: {}", thread_name));

let predictor = match output_clone.clone() {
config::Output::Backlight(backlight_output) => backlight_output.predictor,
config::Output::DdcUtil(ddcutil_output) => ddcutil_output.predictor,
};
let thread_name = format!("predictor-{}", output_name);
std::thread::Builder::new()
.name(thread_name.clone())
Expand All @@ -77,13 +81,28 @@ fn main() {
}
};

let controller = predictor::Controller::new(
prediction_tx,
user_rx,
als_rx,
true,
&output_name,
);
let controller = match predictor {
config::Predictor::Manual { thresholds } => {
Box::new(predictor::controller::manual::Controller::new(
prediction_tx,
user_rx,
als_rx,
thresholds,
))
as Box<dyn predictor::Controller>
}
config::Predictor::Adaptive => {
Box::new(predictor::controller::adaptive::Controller::new(
prediction_tx,
user_rx,
als_rx,
true,
&output_name,
))
as Box<dyn predictor::Controller>
}
};

frame_capturer.run(&output_name, controller)
})
.unwrap_or_else(|_| panic!("Unable to start thread: {}", thread_name));
Expand Down Expand Up @@ -122,7 +141,7 @@ fn main() {
.expect("Unable to start thread: als-webcam");
als::webcam::Als::new(webcam_rx, thresholds)
}),
config::Als::None => Box::<als::none::Als>::default(),
config::Als::None { .. } => Box::<als::none::Als>::default(),
};

als::controller::Controller::new(als, als_txs).run();
Expand Down
Loading

0 comments on commit fc2fc47

Please sign in to comment.