From c225554f78896b2a8a6c515e6be8e2cae11849cb Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 9 Aug 2022 20:56:22 -0400 Subject: [PATCH] Define complete Rust back-end API and functionality --- .gitignore | 1 + powertools-rs/Cargo.lock | 113 +++++++++++++- powertools-rs/Cargo.toml | 10 +- powertools-rs/build.sh | 7 +- powertools-rs/src/api/battery.rs | 64 ++++++++ powertools-rs/src/api/cpu.rs | 194 ++++++++++++++++++++++++ powertools-rs/src/api/gpu.rs | 161 ++++++++++++++++++++ powertools-rs/src/api/mod.rs | 6 + powertools-rs/src/api/utility.rs | 21 +++ powertools-rs/src/main.rs | 123 ++++++++++++++-- powertools-rs/src/persist/battery.rs | 15 ++ powertools-rs/src/persist/cpu.rs | 14 +- powertools-rs/src/persist/error.rs | 14 ++ powertools-rs/src/persist/general.rs | 27 ++-- powertools-rs/src/persist/gpu.rs | 7 +- powertools-rs/src/persist/mod.rs | 7 +- powertools-rs/src/resume_worker.rs | 27 ++++ powertools-rs/src/save_worker.rs | 19 +++ powertools-rs/src/settings/battery.rs | 109 ++++++++++++++ powertools-rs/src/settings/cpu.rs | 203 ++++++++++++++++++++++++-- powertools-rs/src/settings/error.rs | 13 ++ powertools-rs/src/settings/general.rs | 157 ++++++++++++++++---- powertools-rs/src/settings/gpu.rs | 186 +++++++++++++++++++++++ powertools-rs/src/settings/min_max.rs | 29 ++++ powertools-rs/src/settings/mod.rs | 20 ++- powertools-rs/src/settings/traits.rs | 14 ++ powertools-rs/src/state/error.rs | 16 ++ powertools-rs/src/state/mod.rs | 5 + powertools-rs/src/state/traits.rs | 5 + powertools-rs/src/utility.rs | 25 ++++ 30 files changed, 1523 insertions(+), 89 deletions(-) create mode 100644 powertools-rs/src/api/battery.rs create mode 100644 powertools-rs/src/api/cpu.rs create mode 100644 powertools-rs/src/api/gpu.rs create mode 100644 powertools-rs/src/api/mod.rs create mode 100644 powertools-rs/src/api/utility.rs create mode 100644 powertools-rs/src/persist/battery.rs create mode 100644 powertools-rs/src/persist/error.rs create mode 100644 powertools-rs/src/resume_worker.rs create mode 100644 powertools-rs/src/save_worker.rs create mode 100644 powertools-rs/src/settings/battery.rs create mode 100644 powertools-rs/src/settings/error.rs create mode 100644 powertools-rs/src/settings/min_max.rs create mode 100644 powertools-rs/src/settings/traits.rs create mode 100644 powertools-rs/src/state/error.rs create mode 100644 powertools-rs/src/state/mod.rs create mode 100644 powertools-rs/src/state/traits.rs create mode 100644 powertools-rs/src/utility.rs diff --git a/.gitignore b/.gitignore index bef27a7..71e279b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ yalc.lock # rust /powertools-rs/target +/bin diff --git a/powertools-rs/Cargo.lock b/powertools-rs/Cargo.lock index f2a64e5..2f30d12 100644 --- a/powertools-rs/Cargo.lock +++ b/powertools-rs/Cargo.lock @@ -2,6 +2,42 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -66,6 +102,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cpufeatures" version = "0.2.2" @@ -85,6 +130,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.9.0" @@ -251,6 +305,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.8" @@ -437,6 +497,12 @@ dependencies = [ "libc", ] +[[package]] +name = "obfstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2b2cbbfd8defa51ff24450a61d73b3ff3e158484ddd274a883e886e6fbaa78" + [[package]] name = "once_cell" version = "1.13.0" @@ -487,9 +553,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powertools-rs" -version = "0.1.0" +version = "1.0.0" dependencies = [ "log", "serde", @@ -691,6 +769,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.98" @@ -945,6 +1029,16 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "url" version = "2.2.2" @@ -959,11 +1053,13 @@ dependencies = [ [[package]] name = "usdpl-back" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d237439986405621b9b6da350aefcfca9e2b808c10695f55f8b80ccc324b2c0" +checksum = "cbbc0781e83ba990f8239142e33173a2d2548701775f3db66702d1af4fd0319a" dependencies = [ "bytes", + "hex", + "obfstr", "tokio", "usdpl-core", "warp", @@ -971,10 +1067,11 @@ dependencies = [ [[package]] name = "usdpl-core" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd726b9f0121d4449082e3ce73586dea0a0448494031833b7b173e4476f0ea5" +checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394" dependencies = [ + "aes-gcm-siv", "base64", ] @@ -1109,3 +1206,9 @@ name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/powertools-rs/Cargo.toml b/powertools-rs/Cargo.toml index f8047fc..8116352 100644 --- a/powertools-rs/Cargo.toml +++ b/powertools-rs/Cargo.toml @@ -1,15 +1,21 @@ [package] name = "powertools-rs" -version = "0.1.0" +version = "1.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -usdpl-back = { version = "0.5.3", features = ["blocking"]} +usdpl-back = { version = "0.6.0", features = ["blocking"]} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # logging log = "0.4" simplelog = "0.12" + +[features] +default = [] +decky = ["usdpl-back/decky"] +crankshaft = ["usdpl-back/crankshaft"] +encrypt = ["usdpl-back/encrypt"] diff --git a/powertools-rs/build.sh b/powertools-rs/build.sh index d9b898f..808e199 100755 --- a/powertools-rs/build.sh +++ b/powertools-rs/build.sh @@ -1,6 +1,5 @@ #!/bin/bash -cargo build --release -mkdir ../bin -# TODO replace "backend" \/ with binary name -cp ./target/release/backend ../bin/backend +cargo build --release --target x86_64-unknown-linux-musl +mkdir ../bin &> /dev/null +cp ./target/release/powertools-rs ../bin/backend diff --git a/powertools-rs/src/api/battery.rs b/powertools-rs/src/api/battery.rs new file mode 100644 index 0000000..507ed78 --- /dev/null +++ b/powertools-rs/src/api/battery.rs @@ -0,0 +1,64 @@ +use std::sync::{mpsc::Sender, Arc, Mutex}; +use usdpl_back::core::serdes::Primitive; + +use crate::settings::{Battery, OnSet}; +use crate::utility::{unwrap_lock, unwrap_maybe_fatal}; + +/// Current current (ha!) web method +pub fn current_now(_: super::ApiParameterType) -> super::ApiParameterType { + super::utility::map_result(crate::settings::Battery::current_now()) +} + +/// Generate set battery charge rate web method +pub fn set_charge_rate( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(new_val)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "battery"); + settings_lock.charge_rate = Some(*new_val as _); + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + super::utility::map_empty_result( + settings_lock.on_set(), + settings_lock.charge_rate.unwrap(), + ) + } else { + vec!["set_charge_rate missing parameter".into()] + } + } +} + +/// Generate get battery charge rate web method +pub fn get_charge_rate( + settings: Arc>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "battery"); + vec![settings_lock + .charge_rate + .map(|x| x.into()) + .unwrap_or(Primitive::Empty)] + } +} + +/// Generate unset battery charge rate web method +pub fn unset_charge_rate( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |_: super::ApiParameterType| { + let mut settings_lock = unwrap_lock(settings.lock(), "battery"); + settings_lock.charge_rate = None; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + vec![] + } +} diff --git a/powertools-rs/src/api/cpu.rs b/powertools-rs/src/api/cpu.rs new file mode 100644 index 0000000..02d8570 --- /dev/null +++ b/powertools-rs/src/api/cpu.rs @@ -0,0 +1,194 @@ +use std::sync::{mpsc::Sender, Arc, Mutex}; +use usdpl_back::core::serdes::Primitive; + +use crate::settings::{Cpu, OnSet, SettingError, SettingVariant, MinMax}; +use crate::utility::{unwrap_lock, unwrap_maybe_fatal}; + +/// Available CPUs web method +pub fn max_cpus(_: super::ApiParameterType) -> super::ApiParameterType { + super::utility::map_result( + Cpu::cpu_count() + .map(|x| x as u64) + .ok_or_else( + || SettingError { + msg: "Failed to parse CPU count".to_owned(), + setting: SettingVariant::Cpu, + } + ) + ) +} + +/// Generate set CPU online web method +pub fn set_cpu_online( + settings: Arc>>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(index)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "cpu"); + if let Some(cpu) = settings_lock.get_mut(*index as usize) { + if let Some(Primitive::Bool(online)) = params_in.get(1) { + cpu.online = *online; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + super::utility::map_empty_result( + cpu.on_set(), + cpu.online, + ) + } else { + vec!["set_cpu_online missing parameter 1".into()] + } + } else { + vec!["set_cpu_online cpu index out of bounds".into()] + } + } else { + vec!["set_cpu_online missing parameter 0".into()] + } + } +} + +pub fn get_cpus_online( + settings: Arc>>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "cpu"); + let mut output = Vec::with_capacity(settings_lock.len()); + for cpu in settings_lock.as_slice() { + output.push(cpu.online.into()); + } + output + } +} + +pub fn set_clock_limits( + settings: Arc>>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(index)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "cpu"); + if let Some(cpu) = settings_lock.get_mut(*index as usize) { + if let Some(Primitive::F64(min)) = params_in.get(1) { + if let Some(Primitive::F64(max)) = params_in.get(2) { + cpu.clock_limits = Some(MinMax { + min: *min as _, + max: *max as _, + }); + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + match cpu.on_set() { + Ok(_) => vec![ + cpu.clock_limits.as_ref().unwrap().min.into(), + cpu.clock_limits.as_ref().unwrap().max.into(), + ], + Err(e) => vec![e.msg.into()] + } + } else { + vec!["set_clock_limits missing parameter 2".into()] + } + } else { + vec!["set_clock_limits missing parameter 1".into()] + } + } else { + vec!["set_clock_limits cpu index out of bounds".into()] + } + } else { + vec!["set_clock_limits missing parameter 0".into()] + } + } +} + +pub fn get_clock_limits( + settings: Arc>>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(index)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "cpu"); + if let Some(cpu) = settings_lock.get_mut(*index as usize) { + if let Some(min_max) = &cpu.clock_limits { + vec![min_max.max.into(), min_max.min.into()] + } else { + vec![Primitive::Empty, Primitive::Empty] + } + } else { + vec!["get_clock_limits cpu index out of bounds".into()] + } + } else { + vec!["get_clock_limits missing parameter 0".into()] + } + } +} + +pub fn unset_clock_limits( + settings: Arc>>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(index)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "cpu"); + if let Some(cpu) = settings_lock.get_mut(*index as usize) { + cpu.clock_limits = None; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + vec![] + } else { + vec!["get_clock_limits cpu index out of bounds".into()] + } + } else { + vec!["get_clock_limits missing parameter 0".into()] + } + } +} + +pub fn set_cpu_governor( + settings: Arc>>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(index)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "cpu"); + if let Some(cpu) = settings_lock.get_mut(*index as usize) { + if let Some(Primitive::String(governor)) = params_in.get(1) { + cpu.governor = governor.to_owned(); + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + super::utility::map_empty_result( + cpu.on_set(), + &cpu.governor as &str, + ) + } else { + vec!["set_cpu_governor missing parameter 1".into()] + } + } else { + vec!["set_cpu_governor cpu index out of bounds".into()] + } + } else { + vec!["set_cpu_governor missing parameter 0".into()] + } + } +} + +pub fn get_cpu_governors( + settings: Arc>>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "cpu"); + let mut output = Vec::with_capacity(settings_lock.len()); + for cpu in settings_lock.as_slice() { + output.push(cpu.governor.clone().into()); + } + output + } +} diff --git a/powertools-rs/src/api/gpu.rs b/powertools-rs/src/api/gpu.rs new file mode 100644 index 0000000..008f563 --- /dev/null +++ b/powertools-rs/src/api/gpu.rs @@ -0,0 +1,161 @@ +use std::sync::{mpsc::Sender, Arc, Mutex}; +use usdpl_back::core::serdes::Primitive; + +use crate::settings::{Gpu, OnSet, MinMax}; +use crate::utility::{unwrap_lock, unwrap_maybe_fatal}; + +pub fn set_ppt( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(fast_ppt)) = params_in.get(0) { + if let Some(Primitive::F64(slow_ppt)) = params_in.get(1) { + let mut settings_lock = unwrap_lock(settings.lock(), "gpu"); + settings_lock.fast_ppt = Some(*fast_ppt as u64); + settings_lock.slow_ppt = Some(*slow_ppt as u64); + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + match settings_lock.on_set() { + Ok(_) => vec![ + settings_lock.fast_ppt.unwrap().into(), + settings_lock.slow_ppt.unwrap().into() + ], + Err(e) => vec![e.msg.into()], + } + } else { + vec!["set_ppt missing parameter 1".into()] + } + } else { + vec!["set_ppt missing parameter 0".into()] + } + } +} + +pub fn get_ppt( + settings: Arc>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "gpu"); + let fast_ppt = settings_lock.fast_ppt.map(|x| x.into()).unwrap_or(Primitive::Empty); + let slow_ppt = settings_lock.slow_ppt.map(|x| x.into()).unwrap_or(Primitive::Empty); + vec![fast_ppt, slow_ppt] + } +} + +pub fn unset_ppt( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |_: super::ApiParameterType| { + let mut settings_lock = unwrap_lock(settings.lock(), "gpu"); + settings_lock.fast_ppt = None; + settings_lock.slow_ppt = None; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + super::utility::map_empty_result( + settings_lock.on_set(), + Primitive::Empty, + ) + } +} + +pub fn set_clock_limits( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::F64(min)) = params_in.get(0) { + if let Some(Primitive::F64(max)) = params_in.get(1) { + let mut settings_lock = unwrap_lock(settings.lock(), "gpu"); + settings_lock.clock_limits = Some(MinMax { + min: *min as _, + max: *max as _, + }); + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + match settings_lock.on_set() { + Ok(_) => vec![ + settings_lock.clock_limits.as_ref().unwrap().min.into(), + settings_lock.clock_limits.as_ref().unwrap().max.into(), + ], + Err(e) => vec![e.msg.into()] + } + } else { + vec!["set_clock_limits missing parameter 1".into()] + } + } else { + vec!["set_clock_limits missing parameter 0".into()] + } + } +} + +pub fn get_clock_limits( + settings: Arc>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "gpu"); + if let Some(min_max) = &settings_lock.clock_limits { + vec![min_max.max.into(), min_max.min.into()] + } else { + vec![Primitive::Empty, Primitive::Empty] + } + } +} + +pub fn unset_clock_limits( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |_: super::ApiParameterType| { + let mut settings_lock = unwrap_lock(settings.lock(), "gpu"); + settings_lock.clock_limits = None; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + vec![] + } +} + +pub fn set_slow_memory( + settings: Arc>, + saver: Sender<()>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + let saver = Mutex::new(saver); // Sender is not Sync; this is required for safety + move |params_in: super::ApiParameterType| { + if let Some(Primitive::Bool(memory_is_slow)) = params_in.get(0) { + let mut settings_lock = unwrap_lock(settings.lock(), "gpu"); + settings_lock.slow_memory = *memory_is_slow; + unwrap_maybe_fatal( + unwrap_lock(saver.lock(), "save channel").send(()), + "Failed to send on save channel", + ); + super::utility::map_empty_result( + settings_lock.on_set(), + settings_lock.slow_memory, + ) + } else { + vec!["set_slow_memory missing parameter 0".into()] + } + } +} + +pub fn get_slow_memory( + settings: Arc>, +) -> impl Fn(super::ApiParameterType) -> super::ApiParameterType { + move |_: super::ApiParameterType| { + let settings_lock = unwrap_lock(settings.lock(), "cpu"); + vec![settings_lock.slow_memory.into()] + } +} diff --git a/powertools-rs/src/api/mod.rs b/powertools-rs/src/api/mod.rs new file mode 100644 index 0000000..ed8f332 --- /dev/null +++ b/powertools-rs/src/api/mod.rs @@ -0,0 +1,6 @@ +pub mod battery; +pub mod cpu; +pub mod gpu; +mod utility; + +pub(super) type ApiParameterType = Vec; diff --git a/powertools-rs/src/api/utility.rs b/powertools-rs/src/api/utility.rs new file mode 100644 index 0000000..a2fbfb1 --- /dev/null +++ b/powertools-rs/src/api/utility.rs @@ -0,0 +1,21 @@ +use std::convert::Into; +use usdpl_back::core::serdes::Primitive; + +use crate::settings::SettingError; + +pub fn map_result>(result: Result) -> super::ApiParameterType { + match result { + Ok(val) => vec![val.into()], + Err(e) => vec![e.msg.into()], + } +} + +pub fn map_empty_result>( + result: Result<(), SettingError>, + success: T, +) -> super::ApiParameterType { + match result { + Ok(_) => vec![success.into()], + Err(e) => vec![e.msg.into()], + } +} diff --git a/powertools-rs/src/main.rs b/powertools-rs/src/main.rs index 6f1edab..4565349 100644 --- a/powertools-rs/src/main.rs +++ b/powertools-rs/src/main.rs @@ -1,35 +1,132 @@ +mod api; mod persist; mod settings; +mod state; -use simplelog::{WriteLogger, LevelFilter}; +mod resume_worker; +mod save_worker; +mod utility; + +use simplelog::{LevelFilter, WriteLogger}; -use usdpl_back::Instance; use usdpl_back::core::serdes::Primitive; +use usdpl_back::Instance; const PORT: u16 = 44443; const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME"); const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION"); +const DEFAULT_SETTINGS_FILE: &str = "default_settings.json"; + fn main() -> Result<(), ()> { let log_filepath = format!("/tmp/{}.log", PACKAGE_NAME); WriteLogger::init( - #[cfg(debug_assertions)]{LevelFilter::Debug}, - #[cfg(not(debug_assertions))]{LevelFilter::Info}, + #[cfg(debug_assertions)] + { + LevelFilter::Debug + }, + #[cfg(not(debug_assertions))] + { + LevelFilter::Info + }, Default::default(), - std::fs::File::create(&log_filepath).unwrap() - ).unwrap(); + std::fs::File::create(&log_filepath).unwrap(), + ) + .unwrap(); log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION); - - let default_settings: settings::Settings = persist::SettingsJson::open( - settings_dir().join("default_settings.json") - ).unwrap_or_default().into(); - + + let default_settings = persist::SettingsJson::open(settings_dir().join(DEFAULT_SETTINGS_FILE)) + .map(|settings| settings::Settings::from_json(settings, DEFAULT_SETTINGS_FILE.into())) + .unwrap_or_else(|_| settings::Settings::system_default(DEFAULT_SETTINGS_FILE.into())); + log::debug!("Settings: {:?}", default_settings); - + + let (_save_handle, save_sender) = save_worker::spawn(default_settings.clone()); + let _resume_handle = resume_worker::spawn(default_settings.clone()); + Instance::new(PORT) - .register("hello", |_: Vec| vec![format!("Hello {}", PACKAGE_NAME).into()]) + .register("hello", |_: Vec| { + vec![format!("Hello {}", PACKAGE_NAME).into()] + }) + // battery API functions + .register("BATTERY_current_now", api::battery::current_now) + .register( + "BATTERY_set_charge_rate", + api::battery::set_charge_rate(default_settings.battery.clone(), save_sender.clone()), + ) + .register( + "BATTERY_get_charge_rate", + api::battery::get_charge_rate(default_settings.battery.clone()), + ) + .register( + "BATTERY_unset_charge_rate", + api::battery::unset_charge_rate(default_settings.battery.clone(), save_sender.clone()), + ) + // cpu API functions + .register("CPU_count", api::cpu::max_cpus) + .register( + "CPU_set_online", + api::cpu::set_cpu_online(default_settings.cpus.clone(), save_sender.clone()) + ) + .register( + "CPU_get_online", + api::cpu::get_cpus_online(default_settings.cpus.clone()) + ) + .register( + "CPU_set_clock_limits", + api::cpu::set_clock_limits(default_settings.cpus.clone(), save_sender.clone()) + ) + .register( + "CPU_get_clock_limits", + api::cpu::get_clock_limits(default_settings.cpus.clone()) + ) + .register( + "CPU_unset_clock_limits", + api::cpu::unset_clock_limits(default_settings.cpus.clone(), save_sender.clone()) + ) + .register( + "CPU_set_governor", + api::cpu::set_cpu_governor(default_settings.cpus.clone(), save_sender.clone()) + ) + .register( + "CPU_get_governors", + api::cpu::get_cpu_governors(default_settings.cpus.clone()) + ) + // gpu API functions + .register( + "GPU_set_ppt", + api::gpu::set_ppt(default_settings.gpu.clone(), save_sender.clone()) + ) + .register( + "GPU_get_ppt", + api::gpu::get_ppt(default_settings.gpu.clone()) + ) + .register( + "GPU_unset_ppt", + api::gpu::unset_ppt(default_settings.gpu.clone(), save_sender.clone()) + ) + .register( + "GPU_set_clock_limits", + api::gpu::set_clock_limits(default_settings.gpu.clone(), save_sender.clone()) + ) + .register( + "GPU_get_clock_limits", + api::gpu::get_clock_limits(default_settings.gpu.clone()) + ) + .register( + "GPU_unset_clock_limits", + api::gpu::unset_clock_limits(default_settings.gpu.clone(), save_sender.clone()) + ) + .register( + "GPU_set_slow_memory", + api::gpu::set_slow_memory(default_settings.gpu.clone(), save_sender.clone()) + ) + .register( + "GPU_get_slow_memory", + api::gpu::get_slow_memory(default_settings.gpu.clone()) + ) .run_blocking() } diff --git a/powertools-rs/src/persist/battery.rs b/powertools-rs/src/persist/battery.rs new file mode 100644 index 0000000..259e327 --- /dev/null +++ b/powertools-rs/src/persist/battery.rs @@ -0,0 +1,15 @@ +use std::default::Default; +//use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct BatteryJson { + pub charge_rate: Option, +} + +impl Default for BatteryJson { + fn default() -> Self { + Self { charge_rate: None } + } +} diff --git a/powertools-rs/src/persist/cpu.rs b/powertools-rs/src/persist/cpu.rs index c2cb66f..0af2781 100644 --- a/powertools-rs/src/persist/cpu.rs +++ b/powertools-rs/src/persist/cpu.rs @@ -1,27 +1,25 @@ use std::default::Default; //use std::fmt::Display; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -const SCALING_FREQUENCIES: &[u64] = &[1700000, 2400000, 2800000]; +use super::MinMaxJson; + +//const SCALING_FREQUENCIES: &[u64] = &[1700000, 2400000, 2800000]; #[derive(Serialize, Deserialize)] pub struct CpuJson { pub online: bool, - pub max_boost: u64, - pub min_boost: u64, + pub clock_limits: Option>, pub governor: String, - pub boost: bool, } impl Default for CpuJson { fn default() -> Self { Self { online: true, - max_boost: SCALING_FREQUENCIES[SCALING_FREQUENCIES.len() - 1], - min_boost: SCALING_FREQUENCIES[0], + clock_limits: None, governor: "schedutil".to_owned(), - boost: true, } } } diff --git a/powertools-rs/src/persist/error.rs b/powertools-rs/src/persist/error.rs new file mode 100644 index 0000000..2dcb6fa --- /dev/null +++ b/powertools-rs/src/persist/error.rs @@ -0,0 +1,14 @@ +#[derive(Debug)] +pub enum JsonError { + Serde(serde_json::Error), + Io(std::io::Error), +} + +impl std::fmt::Display for JsonError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Serde(e) => (e as &dyn std::fmt::Display).fmt(f), + Self::Io(e) => (e as &dyn std::fmt::Display).fmt(f), + } + } +} diff --git a/powertools-rs/src/persist/general.rs b/powertools-rs/src/persist/general.rs index aafc146..800ec86 100644 --- a/powertools-rs/src/persist/general.rs +++ b/powertools-rs/src/persist/general.rs @@ -1,25 +1,29 @@ use std::default::Default; -use std::fmt::Display; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -use super::{CpuJson, GpuJson}; +use super::JsonError; +use super::{BatteryJson, CpuJson, GpuJson}; #[derive(Serialize, Deserialize)] pub struct SettingsJson { pub version: u64, + pub name: String, pub persistent: bool, pub cpus: Vec, pub gpu: GpuJson, + pub battery: BatteryJson, } impl Default for SettingsJson { fn default() -> Self { Self { version: 0, + name: "default".to_owned(), persistent: false, cpus: Vec::with_capacity(8), gpu: GpuJson::default(), + battery: BatteryJson::default(), } } } @@ -40,17 +44,8 @@ impl SettingsJson { } } -#[derive(Debug)] -pub enum JsonError { - Serde(serde_json::Error), - Io(std::io::Error), -} - -impl Display for JsonError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Serde(e) => (e as &dyn Display).fmt(f), - Self::Io(e) => (e as &dyn Display).fmt(f), - } - } +#[derive(Serialize, Deserialize)] +pub struct MinMaxJson { + pub max: T, + pub min: T, } diff --git a/powertools-rs/src/persist/gpu.rs b/powertools-rs/src/persist/gpu.rs index 41c3c84..ecb352f 100644 --- a/powertools-rs/src/persist/gpu.rs +++ b/powertools-rs/src/persist/gpu.rs @@ -1,12 +1,15 @@ use std::default::Default; //use std::fmt::Display; -use serde::{Serialize, Deserialize}; +use super::MinMaxJson; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct GpuJson { pub fast_ppt: Option, pub slow_ppt: Option, + pub clock_limits: Option>, + pub slow_memory: bool, } impl Default for GpuJson { @@ -14,6 +17,8 @@ impl Default for GpuJson { Self { fast_ppt: None, slow_ppt: None, + clock_limits: None, + slow_memory: false, } } } diff --git a/powertools-rs/src/persist/mod.rs b/powertools-rs/src/persist/mod.rs index ab8b31b..5b70ff5 100644 --- a/powertools-rs/src/persist/mod.rs +++ b/powertools-rs/src/persist/mod.rs @@ -1,7 +1,12 @@ +mod battery; mod cpu; +mod error; mod general; mod gpu; +pub use battery::BatteryJson; pub use cpu::CpuJson; -pub use general::SettingsJson; +pub use general::{MinMaxJson, SettingsJson}; pub use gpu::GpuJson; + +pub use error::JsonError; diff --git a/powertools-rs/src/resume_worker.rs b/powertools-rs/src/resume_worker.rs new file mode 100644 index 0000000..9bded8f --- /dev/null +++ b/powertools-rs/src/resume_worker.rs @@ -0,0 +1,27 @@ +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use crate::settings::{OnResume, Settings}; +use crate::utility::unwrap_maybe_fatal; + +const ALLOWED_ERROR: f64 = 0.001; + +pub fn spawn(settings: Settings) -> JoinHandle<()> { + thread::spawn(move || { + let duration = Duration::from_millis(5000); + let mut start = Instant::now(); + loop { + thread::sleep(duration); + let old_start = start.elapsed(); + start = Instant::now(); + if old_start.as_secs_f64() > duration.as_secs_f64() * (1.0 + ALLOWED_ERROR) { + // has just resumed from sleep + unwrap_maybe_fatal(settings.on_resume(), "On resume failure"); + log::info!( + "OnResume completed after sleeping for {}s", + old_start.as_secs_f32() + ); + } + } + }) +} diff --git a/powertools-rs/src/save_worker.rs b/powertools-rs/src/save_worker.rs new file mode 100644 index 0000000..221f91f --- /dev/null +++ b/powertools-rs/src/save_worker.rs @@ -0,0 +1,19 @@ +use std::sync::mpsc::{self, Receiver, Sender}; +use std::thread::{self, JoinHandle}; + +use crate::persist::SettingsJson; +use crate::settings::Settings; +use crate::utility::{unwrap_lock, unwrap_maybe_fatal}; + +pub fn spawn(settings: Settings) -> (JoinHandle<()>, Sender<()>) { + let (sender, receiver): (Sender<()>, Receiver<()>) = mpsc::channel(); + let worker = thread::spawn(move || { + for _ in receiver.iter() { + let save_path = unwrap_lock(settings.general.lock(), "general").path.clone(); + let save_json: SettingsJson = settings.clone().into(); + unwrap_maybe_fatal(save_json.save(&save_path), "Failed to save settings"); + log::debug!("Saved settings to {}", save_path.display()); + } + }); + (worker, sender) +} diff --git a/powertools-rs/src/settings/battery.rs b/powertools-rs/src/settings/battery.rs new file mode 100644 index 0000000..39d9b7d --- /dev/null +++ b/powertools-rs/src/settings/battery.rs @@ -0,0 +1,109 @@ +use std::convert::Into; + +use super::{OnResume, OnSet, SettingError, SettingsRange}; +use crate::persist::BatteryJson; + +#[derive(Debug, Clone)] +pub struct Battery { + pub charge_rate: Option, +} + +const BATTERY_CHARGE_RATE_PATH: &str = "/sys/class/hwmon/hwmon5/maximum_battery_charge_rate"; // write-only +const BATTERY_CURRENT_NOW_PATH: &str = "/sys/class/power_supply/BAT1/current_now"; // read-only + +impl Battery { + #[inline] + pub fn from_json(other: BatteryJson, version: u64) -> Self { + match version { + 0 => Self { + charge_rate: other.charge_rate, + }, + _ => Self { + charge_rate: other.charge_rate, + }, + } + } + + fn set_all(&self) -> Result<(), SettingError> { + if let Some(charge_rate) = self.charge_rate { + usdpl_back::api::files::write_single(BATTERY_CHARGE_RATE_PATH, charge_rate).map_err( + |e| SettingError { + msg: format!("Failed to write to `{}`: {}", BATTERY_CHARGE_RATE_PATH, e), + setting: super::SettingVariant::Battery, + }, + ) + } else { + Ok(()) + } + } + + fn clamp_all(&mut self) { + let min = Self::min(); + let max = Self::max(); + if let Some(charge_rate) = &mut self.charge_rate { + *charge_rate = (*charge_rate).clamp(min.charge_rate.unwrap(), max.charge_rate.unwrap()); + } + } + + pub fn current_now() -> Result { + match usdpl_back::api::files::read_single::<_, u64, _>(BATTERY_CURRENT_NOW_PATH) { + Err((Some(e), None)) => Err(SettingError { + msg: format!("Failed to read from `{}`: {}", BATTERY_CURRENT_NOW_PATH, e), + setting: super::SettingVariant::Battery, + }), + Err((None, Some(e))) => Err(SettingError { + msg: format!("Failed to read from `{}`: {}", BATTERY_CURRENT_NOW_PATH, e), + setting: super::SettingVariant::Battery, + }), + Err(_) => panic!( + "Invalid error while reading from `{}`", + BATTERY_CURRENT_NOW_PATH + ), + // this value is in uA, while it's set in mA + // so convert this to mA for consistency + Ok(val) => Ok(val / 1000), + } + } + + pub fn system_default() -> Self { + Self { charge_rate: None } + } +} + +impl Into for Battery { + #[inline] + fn into(self) -> BatteryJson { + BatteryJson { + charge_rate: self.charge_rate, + } + } +} + +impl OnSet for Battery { + fn on_set(&mut self) -> Result<(), SettingError> { + self.clamp_all(); + self.set_all() + } +} + +impl OnResume for Battery { + fn on_resume(&self) -> Result<(), SettingError> { + self.set_all() + } +} + +impl SettingsRange for Battery { + #[inline] + fn max() -> Self { + Self { + charge_rate: Some(2500), + } + } + + #[inline] + fn min() -> Self { + Self { + charge_rate: Some(250), + } + } +} diff --git a/powertools-rs/src/settings/cpu.rs b/powertools-rs/src/settings/cpu.rs index bf7c72a..aca68e8 100644 --- a/powertools-rs/src/settings/cpu.rs +++ b/powertools-rs/src/settings/cpu.rs @@ -1,34 +1,162 @@ use std::convert::Into; +use super::MinMax; +use super::{OnResume, OnSet, SettingError, SettingsRange}; use crate::persist::CpuJson; #[derive(Debug, Clone)] pub struct Cpu { pub online: bool, - pub max_boost: u64, - pub min_boost: u64, + pub clock_limits: Option>, pub governor: String, - pub boost: bool, + index: usize, } +const CPU_CLOCK_LIMITS_PATH: &str = "/sys/class/drm/card0/device/pp_od_clk_voltage"; +const CPU_FORCE_LIMITS_PATH: &str = "/sys/class/drm/card0/device/power_dpm_force_performance_level"; + +const CPU_PRESENT_PATH: &str = "/sys/devices/system/cpu/present"; + impl Cpu { #[inline] - pub fn from_json(other: CpuJson, version: u64) -> Self { + pub fn from_json(other: CpuJson, version: u64, i: usize) -> Self { match version { 0 => Self { online: other.online, - max_boost: other.max_boost, - min_boost: other.min_boost, + clock_limits: other.clock_limits.map(|x| MinMax::from_json(x, version)), governor: other.governor, - boost: other.boost, + index: i, }, _ => Self { online: other.online, - max_boost: other.max_boost, - min_boost: other.min_boost, + clock_limits: other.clock_limits.map(|x| MinMax::from_json(x, version)), governor: other.governor, - boost: other.boost, + index: i, + }, + } + } + + fn set_all(&self) -> Result<(), SettingError> { + // set cpu online/offline + if self.index != 0 { // cpu0 cannot be disabled + let online_path = cpu_online_path(self.index); + usdpl_back::api::files::write_single(&online_path, self.online as u8).map_err(|e| { + SettingError { + msg: format!("Failed to write to `{}`: {}", &online_path, e), + setting: super::SettingVariant::Cpu, + } + })?; + } + // set clock limits + if let Some(clock_limits) = &self.clock_limits { + // set manual control + usdpl_back::api::files::write_single(CPU_FORCE_LIMITS_PATH, "manual").map_err(|e| { + SettingError { + msg: format!( + "Failed to write `manual` to `{}`: {}", + CPU_FORCE_LIMITS_PATH, e + ), + setting: super::SettingVariant::Cpu, + } + })?; + // max clock + let payload_max = format!("p {} 1 {}", self.index / 2, clock_limits.max); + usdpl_back::api::files::write_single(CPU_CLOCK_LIMITS_PATH, &payload_max).map_err( + |e| SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &payload_max, CPU_CLOCK_LIMITS_PATH, e + ), + setting: super::SettingVariant::Cpu, + }, + )?; + // min clock + let payload_min = format!("p {} 0 {}", self.index / 2, clock_limits.min); + usdpl_back::api::files::write_single(CPU_CLOCK_LIMITS_PATH, &payload_min).map_err( + |e| SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &payload_min, CPU_CLOCK_LIMITS_PATH, e + ), + setting: super::SettingVariant::Cpu, + }, + )?; + // commit changes + usdpl_back::api::files::write_single(CPU_CLOCK_LIMITS_PATH, "c").map_err(|e| { + SettingError { + msg: format!("Failed to write `c` to `{}`: {}", CPU_CLOCK_LIMITS_PATH, e), + setting: super::SettingVariant::Cpu, + } + })?; + } else { + // disable manual clock limits + usdpl_back::api::files::write_single(CPU_FORCE_LIMITS_PATH, "auto").map_err(|e| { + SettingError { + msg: format!( + "Failed to write `auto` to `{}`: {}", + CPU_FORCE_LIMITS_PATH, e + ), + setting: super::SettingVariant::Cpu, + } + })?; + } + // set governor + let governor_path = cpu_governor_path(self.index); + usdpl_back::api::files::write_single(&governor_path, &self.governor).map_err(|e| { + SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &self.governor, &governor_path, e + ), + setting: super::SettingVariant::Cpu, } + })?; + Ok(()) + } + + fn clamp_all(&mut self) { + let min = Self::min(); + let max = Self::max(); + if let Some(clock_limits) = &mut self.clock_limits { + let max_boost = max.clock_limits.as_ref().unwrap(); + let min_boost = min.clock_limits.as_ref().unwrap(); + clock_limits.min = clock_limits.min.clamp(min_boost.min, max_boost.min); + clock_limits.max = clock_limits.max.clamp(min_boost.max, max_boost.max); + } + } + + fn from_sys(index: usize) -> Self { + Self { + online: usdpl_back::api::files::read_single(cpu_online_path(index)).unwrap_or(1u8) != 0, + clock_limits: None, + governor: usdpl_back::api::files::read_single(cpu_governor_path(index)) + .unwrap_or("schedutil".to_owned()), + index: index, + } + } + + pub fn cpu_count() -> Option { + let mut data: String = usdpl_back::api::files::read_single(CPU_PRESENT_PATH) + .unwrap_or_else(|_| "0-7".to_string() /* Steam Deck's default */); + if let Some(dash_index) = data.find('-') { + let data = data.split_off(dash_index + 1); + if let Ok(max_cpu) = data.parse::() { + return Some(max_cpu); + } + } + log::warn!("Failed to parse CPU info from kernel, is Tux evil?"); + None + } + + pub fn system_default() -> Vec { + if let Some(max_cpu) = Self::cpu_count() { + let mut cpus = Vec::with_capacity(max_cpu + 1); + for i in 0..=max_cpu { + cpus.push(Self::from_sys(i)); + } + cpus + } else { + Vec::with_capacity(0) } } } @@ -38,10 +166,59 @@ impl Into for Cpu { fn into(self) -> CpuJson { CpuJson { online: self.online, - max_boost: self.max_boost, - min_boost: self.min_boost, + clock_limits: self.clock_limits.map(|x| x.into()), governor: self.governor, - boost: self.boost, } } } + +impl OnSet for Cpu { + fn on_set(&mut self) -> Result<(), SettingError> { + self.clamp_all(); + self.set_all() + } +} + +impl OnResume for Cpu { + fn on_resume(&self) -> Result<(), SettingError> { + self.set_all() + } +} + +impl SettingsRange for Cpu { + #[inline] + fn max() -> Self { + Self { + online: true, + clock_limits: Some(MinMax { + max: 3500, + min: 3500, + }), + governor: "schedutil".to_owned(), + index: usize::MAX, + } + } + + #[inline] + fn min() -> Self { + Self { + online: false, + clock_limits: Some(MinMax { max: 500, min: 1400 }), + governor: "schedutil".to_owned(), + index: 0, + } + } +} + +#[inline] +fn cpu_online_path(index: usize) -> String { + format!("/sys/devices/system/cpu/cpu{}/online", index) +} + +#[inline] +fn cpu_governor_path(index: usize) -> String { + format!( + "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor", + index + ) +} diff --git a/powertools-rs/src/settings/error.rs b/powertools-rs/src/settings/error.rs new file mode 100644 index 0000000..378fa11 --- /dev/null +++ b/powertools-rs/src/settings/error.rs @@ -0,0 +1,13 @@ +use super::SettingVariant; + +#[derive(Debug, Clone)] +pub struct SettingError { + pub msg: String, + pub setting: SettingVariant, +} + +impl std::fmt::Display for SettingError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} setting error: {}", self.setting, self.msg) + } +} diff --git a/powertools-rs/src/settings/general.rs b/powertools-rs/src/settings/general.rs index c997159..b7c6442 100644 --- a/powertools-rs/src/settings/general.rs +++ b/powertools-rs/src/settings/general.rs @@ -1,43 +1,150 @@ -use std::convert::{Into, From}; +use std::convert::Into; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; -use crate::persist::SettingsJson; -use super::{Cpu, Gpu}; +use super::{Battery, Cpu, Gpu}; +use super::{OnResume, OnSet, SettingError}; +use crate::persist::{CpuJson, SettingsJson}; +use crate::utility::unwrap_lock; const LATEST_VERSION: u64 = 0; -#[derive(Debug, Clone)] -pub struct Settings { - pub persistent: bool, - pub cpus: Vec, - pub gpu: Gpu, +#[derive(Debug, Clone, Copy)] +pub enum SettingVariant { + Battery, + Cpu, + Gpu, + General, } -impl From for Settings { - #[inline] - fn from(mut other: SettingsJson) -> Self { - match other.version { - 0 => Self { - persistent: other.persistent, - cpus: other.cpus.drain(..).map(|cpu| Cpu::from_json(cpu, other.version)).collect(), - gpu: Gpu::from_json(other.gpu, other.version), - }, - _ => Self { - persistent: other.persistent, - cpus: other.cpus.drain(..).map(|cpu| Cpu::from_json(cpu, other.version)).collect(), - gpu: Gpu::from_json(other.gpu, other.version), +impl std::fmt::Display for SettingVariant { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Battery => write!(f, "Battery"), + Self::Cpu => write!(f, "CPU"), + Self::Gpu => write!(f, "GPU"), + Self::General => write!(f, "General"), + } + } +} + +#[derive(Debug, Clone)] +pub struct General { + pub persistent: bool, + pub path: PathBuf, + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct Settings { + pub general: Arc>, + pub cpus: Arc>>, + pub gpu: Arc>, + pub battery: Arc>, +} + +impl OnSet for Settings { + fn on_set(&mut self) -> Result<(), SettingError> { + unwrap_lock(self.battery.lock(), "battery").on_set()?; + { + // cpu lock scope + let mut cpu_lock = unwrap_lock(self.cpus.lock(), "cpu"); + for cpu in cpu_lock.iter_mut() { + cpu.on_set()?; } } + unwrap_lock(self.gpu.lock(), "gpu").on_set()?; + { + // general lock scope + let gen_lock = unwrap_lock(self.general.lock(), "general"); + if !gen_lock.persistent && gen_lock.path.exists() { + std::fs::remove_file(&gen_lock.path).map_err(|e| SettingError { + msg: format!("Failed to delete `{}`: {}", gen_lock.path.display(), e), + setting: SettingVariant::General, + })?; + } + } + Ok(()) + } +} + +impl Settings { + #[inline] + pub fn from_json(other: SettingsJson, json_path: PathBuf) -> Self { + match other.version { + 0 => Self { + general: Arc::new(Mutex::new(General { + persistent: other.persistent, + path: json_path, + name: other.name, + })), + cpus: Arc::new(Mutex::new(Self::convert_cpus(other.cpus, other.version))), + gpu: Arc::new(Mutex::new(Gpu::from_json(other.gpu, other.version))), + battery: Arc::new(Mutex::new(Battery::from_json(other.battery, other.version))), + }, + _ => Self { + general: Arc::new(Mutex::new(General { + persistent: other.persistent, + path: json_path, + name: other.name, + })), + cpus: Arc::new(Mutex::new(Self::convert_cpus(other.cpus, other.version))), + gpu: Arc::new(Mutex::new(Gpu::from_json(other.gpu, other.version))), + battery: Arc::new(Mutex::new(Battery::from_json(other.battery, other.version))), + }, + } + } + + fn convert_cpus(mut cpus: Vec, version: u64) -> Vec { + let mut result = Vec::with_capacity(cpus.len()); + for (i, cpu) in cpus.drain(..).enumerate() { + result.push(Cpu::from_json(cpu, version, i)); + } + result + } + + pub fn system_default(json_path: PathBuf) -> Self { + Self { + general: Arc::new(Mutex::new(General { + persistent: false, + path: json_path, + name: "".to_owned(), + })), + cpus: Arc::new(Mutex::new(Cpu::system_default())), + gpu: Arc::new(Mutex::new(Gpu::system_default())), + battery: Arc::new(Mutex::new(Battery::system_default())), + } + } +} + +impl OnResume for Settings { + fn on_resume(&self) -> Result<(), SettingError> { + unwrap_lock(self.battery.lock(), "battery").on_resume()?; + { + let mut cpu_lock = unwrap_lock(self.cpus.lock(), "cpu"); + for cpu in cpu_lock.iter_mut() { + cpu.on_resume()?; + } + } + unwrap_lock(self.gpu.lock(), "gpu").on_resume()?; + Ok(()) } } impl Into for Settings { #[inline] - fn into(mut self) -> SettingsJson { + fn into(self) -> SettingsJson { SettingsJson { version: LATEST_VERSION, - persistent: self.persistent, - cpus: self.cpus.drain(..).map(|cpu| cpu.into()).collect(), - gpu: self.gpu.into() + name: unwrap_lock(self.general.lock(), "general").name.clone(), + persistent: unwrap_lock(self.general.lock(), "general").persistent, + cpus: unwrap_lock(self.cpus.lock(), "cpu") + .clone() + .drain(..) + .map(|cpu| cpu.into()) + .collect(), + gpu: unwrap_lock(self.gpu.lock(), "gpu").clone().into(), + battery: unwrap_lock(self.battery.lock(), "battery").clone().into(), } } } diff --git a/powertools-rs/src/settings/gpu.rs b/powertools-rs/src/settings/gpu.rs index 4432313..147d8de 100644 --- a/powertools-rs/src/settings/gpu.rs +++ b/powertools-rs/src/settings/gpu.rs @@ -1,13 +1,25 @@ use std::convert::Into; +use super::MinMax; +use super::{OnResume, OnSet, SettingError, SettingsRange}; use crate::persist::GpuJson; +const SLOW_PPT: u8 = 1; +const FAST_PPT: u8 = 2; + #[derive(Debug, Clone)] pub struct Gpu { pub fast_ppt: Option, pub slow_ppt: Option, + pub clock_limits: Option>, + pub slow_memory: bool, } +// same as CPU +const GPU_CLOCK_LIMITS_PATH: &str = "/sys/class/drm/card0/device/pp_od_clk_voltage"; +const GPU_FORCE_LIMITS_PATH: &str = "/sys/class/drm/card0/device/power_dpm_force_performance_level"; +const GPU_MEMORY_DOWNCLOCK_PATH: &str = "/sys/class/drm/card0/device/pp_dpm_fclk"; + impl Gpu { #[inline] pub fn from_json(other: GpuJson, version: u64) -> Self { @@ -15,11 +27,140 @@ impl Gpu { 0 => Self { fast_ppt: other.fast_ppt, slow_ppt: other.slow_ppt, + clock_limits: other.clock_limits.map(|x| MinMax::from_json(x, version)), + slow_memory: other.slow_memory, }, _ => Self { fast_ppt: other.fast_ppt, slow_ppt: other.slow_ppt, + clock_limits: other.clock_limits.map(|x| MinMax::from_json(x, version)), + slow_memory: other.slow_memory, + }, + } + } + + fn set_all(&self) -> Result<(), SettingError> { + // set fast PPT + if let Some(fast_ppt) = &self.fast_ppt { + let fast_ppt_path = gpu_power_path(FAST_PPT); + usdpl_back::api::files::write_single(&fast_ppt_path, fast_ppt).map_err(|e| { + SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + fast_ppt, &fast_ppt_path, e + ), + setting: super::SettingVariant::Gpu, + } + })?; + } + // set slow PPT + if let Some(slow_ppt) = &self.slow_ppt { + let slow_ppt_path = gpu_power_path(SLOW_PPT); + usdpl_back::api::files::write_single(&slow_ppt_path, slow_ppt).map_err(|e| { + SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + slow_ppt, &slow_ppt_path, e + ), + setting: super::SettingVariant::Gpu, + } + })?; + } + // settings using force_performance_level + if self.clock_limits.is_some() || self.slow_memory { + // set manual control + usdpl_back::api::files::write_single(GPU_FORCE_LIMITS_PATH, "manual").map_err(|e| { + SettingError { + msg: format!( + "Failed to write `manual` to `{}`: {}", + GPU_FORCE_LIMITS_PATH, e + ), + setting: super::SettingVariant::Gpu, + } + })?; + // set clock limits + if let Some(clock_limits) = &self.clock_limits { + // max clock + let payload_max = format!("s 1 {}", clock_limits.max); + usdpl_back::api::files::write_single(GPU_CLOCK_LIMITS_PATH, &payload_max).map_err( + |e| SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &payload_max, GPU_CLOCK_LIMITS_PATH, e + ), + setting: super::SettingVariant::Gpu, + }, + )?; + // min clock + let payload_min = format!("s 0 {}", clock_limits.min); + usdpl_back::api::files::write_single(GPU_CLOCK_LIMITS_PATH, &payload_min).map_err( + |e| SettingError { + msg: format!( + "Failed to write `{}` to `{}`: {}", + &payload_min, GPU_CLOCK_LIMITS_PATH, e + ), + setting: super::SettingVariant::Gpu, + }, + )?; } + // force downclock of GPU memory (to 400Mhz?) + usdpl_back::api::files::write_single(GPU_MEMORY_DOWNCLOCK_PATH, self.slow_memory as u8) + .map_err(|e| SettingError { + msg: format!("Failed to write to `{}`: {}", GPU_MEMORY_DOWNCLOCK_PATH, e), + setting: super::SettingVariant::Gpu, + })?; + // commit changes + usdpl_back::api::files::write_single(GPU_CLOCK_LIMITS_PATH, "c").map_err(|e| { + SettingError { + msg: format!("Failed to write `c` to `{}`: {}", GPU_CLOCK_LIMITS_PATH, e), + setting: super::SettingVariant::Gpu, + } + })?; + } else { + // disable manual clock limits + usdpl_back::api::files::write_single(GPU_FORCE_LIMITS_PATH, "auto").map_err(|e| { + SettingError { + msg: format!( + "Failed to write `auto` to `{}`: {}", + GPU_FORCE_LIMITS_PATH, e + ), + setting: super::SettingVariant::Gpu, + } + })?; + } + + Ok(()) // TODO + } + + fn clamp_all(&mut self) { + let min = Self::min(); + let max = Self::max(); + if let Some(fast_ppt) = &mut self.fast_ppt { + *fast_ppt = (*fast_ppt).clamp( + *min.fast_ppt.as_ref().unwrap(), + *max.fast_ppt.as_ref().unwrap(), + ); + } + if let Some(slow_ppt) = &mut self.slow_ppt { + *slow_ppt = (*slow_ppt).clamp( + *min.slow_ppt.as_ref().unwrap(), + *max.slow_ppt.as_ref().unwrap(), + ); + } + if let Some(clock_limits) = &mut self.clock_limits { + let max_boost = max.clock_limits.as_ref().unwrap(); + let min_boost = min.clock_limits.as_ref().unwrap(); + clock_limits.min = clock_limits.min.clamp(min_boost.min, max_boost.min); + clock_limits.max = clock_limits.max.clamp(min_boost.max, max_boost.max); + } + } + + pub fn system_default() -> Self { + Self { + fast_ppt: None, + slow_ppt: None, + clock_limits: None, + slow_memory: false, } } } @@ -30,6 +171,51 @@ impl Into for Gpu { GpuJson { fast_ppt: self.fast_ppt, slow_ppt: self.slow_ppt, + clock_limits: self.clock_limits.map(|x| x.into()), + slow_memory: self.slow_memory, } } } + +impl OnSet for Gpu { + fn on_set(&mut self) -> Result<(), SettingError> { + self.clamp_all(); + self.set_all() + } +} + +impl OnResume for Gpu { + fn on_resume(&self) -> Result<(), SettingError> { + self.set_all() + } +} + +impl SettingsRange for Gpu { + #[inline] + fn max() -> Self { + Self { + fast_ppt: Some(30000000), + slow_ppt: Some(29000000), + clock_limits: Some(MinMax { + min: 1600, + max: 1600, + }), + slow_memory: false, + } + } + + #[inline] + fn min() -> Self { + Self { + fast_ppt: Some(0), + slow_ppt: Some(1000000), + clock_limits: Some(MinMax { min: 200, max: 200 }), + slow_memory: true, + } + } +} + +#[inline] +fn gpu_power_path(power_number: u8) -> String { + format!("/sys/class/hwmon/hwmon4/power{}_cap", power_number) +} diff --git a/powertools-rs/src/settings/min_max.rs b/powertools-rs/src/settings/min_max.rs new file mode 100644 index 0000000..00ea8fd --- /dev/null +++ b/powertools-rs/src/settings/min_max.rs @@ -0,0 +1,29 @@ +use std::convert::Into; + +use crate::persist::MinMaxJson; + +#[derive(Debug, Clone)] +pub struct MinMax { + pub max: T, + pub min: T, +} + +impl MinMax { + #[inline] + pub fn from_json>(other: MinMaxJson, _version: u64) -> Self { + Self { + max: other.max.into(), + min: other.min.into(), + } + } +} + +impl, Y> Into> for MinMax { + #[inline] + fn into(self) -> MinMaxJson { + MinMaxJson { + max: self.max.into(), + min: self.min.into(), + } + } +} diff --git a/powertools-rs/src/settings/mod.rs b/powertools-rs/src/settings/mod.rs index adfb0d4..2a70494 100644 --- a/powertools-rs/src/settings/mod.rs +++ b/powertools-rs/src/settings/mod.rs @@ -1,7 +1,25 @@ +mod battery; mod cpu; +mod error; mod general; mod gpu; +mod min_max; +mod traits; +pub use battery::Battery; pub use cpu::Cpu; -pub use general::Settings; +pub use general::{SettingVariant, Settings}; pub use gpu::Gpu; +pub use min_max::MinMax; + +pub use error::SettingError; +pub use traits::{OnResume, OnSet, SettingsRange}; + +#[cfg(test)] +mod tests { + #[test] + fn system_defaults_test() { + let settings = super::Settings::system_default("idc".into()); + println!("Loaded system settings: {:?}", settings); + } +} diff --git a/powertools-rs/src/settings/traits.rs b/powertools-rs/src/settings/traits.rs new file mode 100644 index 0000000..56450df --- /dev/null +++ b/powertools-rs/src/settings/traits.rs @@ -0,0 +1,14 @@ +use super::SettingError; + +pub trait OnSet { + fn on_set(&mut self) -> Result<(), SettingError>; +} + +pub trait OnResume { + fn on_resume(&self) -> Result<(), SettingError>; +} + +pub trait SettingsRange { + fn max() -> Self; + fn min() -> Self; +} diff --git a/powertools-rs/src/state/error.rs b/powertools-rs/src/state/error.rs new file mode 100644 index 0000000..3c777f0 --- /dev/null +++ b/powertools-rs/src/state/error.rs @@ -0,0 +1,16 @@ +use crate::settings::SettingVariant; + +pub struct StateError { + pub msg: String, + pub setting: Option, +} + +impl std::fmt::Display for StateError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(setting) = self.setting { + write!(f, "{} setting state error: {}", setting, self.msg) + } else { + write!(f, "State error: {}", self.msg) + } + } +} diff --git a/powertools-rs/src/state/mod.rs b/powertools-rs/src/state/mod.rs new file mode 100644 index 0000000..2a8befe --- /dev/null +++ b/powertools-rs/src/state/mod.rs @@ -0,0 +1,5 @@ +mod error; +mod traits; + +pub use error::StateError; +pub use traits::OnPoll; diff --git a/powertools-rs/src/state/traits.rs b/powertools-rs/src/state/traits.rs new file mode 100644 index 0000000..8a19314 --- /dev/null +++ b/powertools-rs/src/state/traits.rs @@ -0,0 +1,5 @@ +use super::StateError; + +pub trait OnPoll { + fn on_poll(&self) -> Result<(), StateError>; +} diff --git a/powertools-rs/src/utility.rs b/powertools-rs/src/utility.rs new file mode 100644 index 0000000..f14b992 --- /dev/null +++ b/powertools-rs/src/utility.rs @@ -0,0 +1,25 @@ +use std::fmt::Display; +use std::sync::{LockResult, MutexGuard}; + +pub fn unwrap_maybe_fatal(result: Result, message: &str) -> T { + match result { + Ok(x) => x, + Err(e) => { + log::error!("{}: {}", message, e); + panic!("{}: {}", message, e); + } + } +} + +pub fn unwrap_lock<'a, T: Sized>( + result: LockResult>, + lock_name: &str, +) -> MutexGuard<'a, T> { + match result { + Ok(x) => x, + Err(e) => { + log::error!("Failed to acquire {} lock: {}", lock_name, e); + panic!("Failed to acquire {} lock: {}", lock_name, e); + } + } +}