Start GUI
GUI in iced Basic plot manipulation Working solution Fix documentation Fix build scripts Slice application Change views Very basic GUI Reload config GUI Helper for root tasks - maybe sock files for all services will be better Add save button Xorg gui and save config to target file Documentation and clippy fixes Avoid compiling gui on CI Readme files Add missing dependencies Add missing pgp key Refactor workflow Refactor workflow Add drag and drop
This commit is contained in:
parent
de5f2b77cb
commit
7616ddd8fc
75
.github/workflows/rust.yml
vendored
75
.github/workflows/rust.yml
vendored
@ -10,7 +10,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -19,23 +19,82 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Add key
|
||||
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
- name: Add repo
|
||||
env:
|
||||
UBUNTU: ${{ matrix.os }}
|
||||
run: echo $UBUNTU &&\
|
||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
|
||||
- name: Install binary compressor
|
||||
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
|
||||
- name: Add target
|
||||
run: rustup target install x86_64-unknown-linux-musl
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
- name: Run fmt check
|
||||
run: cargo fmt -- --check
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Add key
|
||||
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
- name: Add repo
|
||||
env:
|
||||
UBUNTU: ${{ matrix.os }}
|
||||
run: echo $UBUNTU &&\
|
||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
|
||||
- name: Install binary compressor
|
||||
run: sudo apt-get update && sudo apt-get install upx-ucl
|
||||
- name: Build
|
||||
run: cargo build --release --verbose --target=x86_64-unknown-linux-musl && strip ./target/x86_64-unknown-linux-musl/release/amdfand && upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
|
||||
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
|
||||
- name: Add target
|
||||
run: rustup target install x86_64-unknown-linux-musl
|
||||
- name: Run clippy
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
build:
|
||||
needs: [clippy, tests]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Add key
|
||||
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
- name: Add repo
|
||||
env:
|
||||
UBUNTU: ${{ matrix.os }}
|
||||
run: echo $UBUNTU &&\
|
||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
|
||||
- name: Install binary compressor
|
||||
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake zip
|
||||
- name: Add target
|
||||
run: rustup target install x86_64-unknown-linux-musl
|
||||
- name: Compile
|
||||
run: bash ./scripts/compile.sh
|
||||
- name: Optimize
|
||||
run: bash ./scripts/build.sh
|
||||
- name: Collect artifacts
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
run: ./scripts/zip-ci.sh $OS
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
with:
|
||||
# Artifact name
|
||||
name: amdfand-${{ matrix.os }}
|
||||
name: binaries-${{ matrix.os }}.zip
|
||||
# A file, directory or wildcard pattern that describes what to upload
|
||||
path: ./target/x86_64-unknown-linux-musl/release/amdfand
|
||||
path: ./binaries-${{ matrix.os }}.zip
|
||||
# The desired behavior if no files are found using the provided path.
|
||||
|
1992
Cargo.lock
generated
1992
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,2 +1,11 @@
|
||||
[workspace]
|
||||
members = ["amdgpu", "amdgpu-config", "amdfand", "amdvold", "amdmond"]
|
||||
members = [
|
||||
"amdgpu",
|
||||
"amdgpu-config",
|
||||
"amdfand",
|
||||
"amdvold",
|
||||
"amdmond",
|
||||
"amdmond-lib",
|
||||
"amdguid",
|
||||
"amdgui-helper",
|
||||
]
|
||||
|
@ -7,18 +7,20 @@ This repository holds couple tools for AMD graphic cards
|
||||
* `amdfand` - fan speed daemon
|
||||
* `amdvold` - voltage and overclocking tool
|
||||
* `amdmond` - monitor daemon
|
||||
* `amdguid` - GUI manager
|
||||
* `amdgui-helper` - daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files
|
||||
|
||||
For more information please check README each of them.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* [ ] Add support for multiple cards
|
||||
* [X] Add support for multiple cards
|
||||
* Multiple services must recognize card even if there's multiple same version cards is installed
|
||||
* Support should be by using `--config` option
|
||||
* [ ] CLI for fan config edit
|
||||
* [ ] CLI for voltage edit
|
||||
* [ ] GUI application using native Rust framework (ex. egui, druid)
|
||||
* [X] GUI application using native Rust framework (ex. egui, druid)
|
||||
|
||||
## :bookmark: License
|
||||
## License
|
||||
|
||||
This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work.
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "amdfand"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
edition = "2018"
|
||||
description = "AMDGPU fan control service"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -9,17 +9,21 @@ categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.8" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["fan"] }
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan"] }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
thiserror = "1.0.30"
|
||||
ron = { version = "0.1.0" }
|
||||
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
pretty_env_logger = { version = "0.4.0" }
|
||||
|
||||
pidlock = { version = "0.1.4" }
|
||||
|
||||
[dev-dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }
|
||||
|
@ -19,10 +19,10 @@ Optional arguments:
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cargo install argonfand
|
||||
cargo install amdfand
|
||||
|
||||
sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed
|
||||
sudo argonfand service # check amdgpu temperature and adjust speed from config file
|
||||
sudo amdfand monitor # print current temperature, current fan speed, min and max fan speed
|
||||
sudo amdfand service # check amdgpu temperature and adjust speed from config file
|
||||
```
|
||||
|
||||
## Config file
|
||||
|
@ -90,7 +90,8 @@ impl Fan {
|
||||
v.into_iter().map(|hw| Self::wrap(hw, config)).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn set_speed(&mut self, speed: f64) -> crate::Result<()> {
|
||||
/// Change fan speed to given value if it's between minimal and maximal value
|
||||
pub fn set_speed(&mut self, speed: f64) -> crate::Result<()> {
|
||||
let min = self.pwm_min() as f64;
|
||||
let max = self.pwm_max() as f64;
|
||||
let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u64;
|
||||
@ -98,18 +99,22 @@ impl Fan {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_manual(&self) -> crate::Result<()> {
|
||||
/// Change gpu fan speed management to manual (amdfand will manage speed) instead of
|
||||
/// GPU embedded manager
|
||||
pub fn write_manual(&self) -> crate::Result<()> {
|
||||
self.hw_mon_write(MODULATION_ENABLED_FILE, 1)
|
||||
.map_err(FanError::ManualSpeedFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_automatic(&self) -> crate::Result<()> {
|
||||
/// Change gpu fan speed management to automatic, speed will be managed by GPU embedded manager
|
||||
pub fn write_automatic(&self) -> crate::Result<()> {
|
||||
self.hw_mon_write("pwm1_enable", 2)
|
||||
.map_err(FanError::AutomaticSpeedFailed)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change fan speed to given value with checking min-max range
|
||||
fn write_pwm(&self, value: u64) -> crate::Result<()> {
|
||||
if self.is_fan_automatic() {
|
||||
self.write_manual()?;
|
||||
@ -119,12 +124,16 @@ impl Fan {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if gpu fan is managed by GPU embedded manager
|
||||
pub fn is_fan_automatic(&self) -> bool {
|
||||
self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE)
|
||||
.map(|s| s.as_str() == PULSE_WIDTH_MODULATION_AUTO)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get maximal GPU temperature from all inputs.
|
||||
/// This is not recommended since GPU can heat differently in different parts and usually only
|
||||
/// temp1 should be taken for consideration.
|
||||
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
|
||||
if let Some(input) = self.temp_input.as_ref() {
|
||||
let value = self.read_gpu_temp(&input.as_string())?;
|
||||
@ -143,7 +152,8 @@ impl Fan {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
||||
/// Read temperature from given input sensor
|
||||
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
||||
let value = self
|
||||
.hw_mon_read(name)?
|
||||
.parse::<u64>()
|
||||
@ -151,6 +161,7 @@ impl Fan {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Read minimal fan speed. Usually this is 0
|
||||
pub fn pwm_min(&mut self) -> u32 {
|
||||
if self.pwm_min.is_none() {
|
||||
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
|
||||
@ -158,6 +169,7 @@ impl Fan {
|
||||
self.pwm_min.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Read minimal fan speed. Usually this is 255
|
||||
pub fn pwm_max(&mut self) -> u32 {
|
||||
if self.pwm_max.is_none() {
|
||||
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
|
||||
|
@ -2,7 +2,10 @@ extern crate log;
|
||||
|
||||
use gumdrop::Options;
|
||||
|
||||
use amdgpu::utils::{ensure_config_dir, hw_mons};
|
||||
use amdgpu::{
|
||||
lock_file::PidLock,
|
||||
utils::{ensure_config_dir, hw_mons},
|
||||
};
|
||||
use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH};
|
||||
|
||||
use crate::command::FanCommand;
|
||||
@ -29,10 +32,16 @@ pub struct Opts {
|
||||
version: bool,
|
||||
#[options(help = "Config location")]
|
||||
config: Option<String>,
|
||||
#[options(
|
||||
help = "Pid file name (exp. card1). This should not be path, only file name without extension"
|
||||
)]
|
||||
pid_file: Option<String>,
|
||||
#[options(command)]
|
||||
command: Option<command::FanCommand>,
|
||||
}
|
||||
|
||||
static DEFAULT_PID_FILE_NAME: &str = "amdfand";
|
||||
|
||||
fn run(config: Config) -> Result<()> {
|
||||
let opts: Opts = Opts::parse_args_default_or_exit();
|
||||
|
||||
@ -47,7 +56,17 @@ fn run(config: Config) -> Result<()> {
|
||||
|
||||
match opts.command {
|
||||
None => service::run(config),
|
||||
Some(FanCommand::Service(_)) => service::run(config),
|
||||
Some(FanCommand::Service(_)) => {
|
||||
let mut pid_file = PidLock::new(
|
||||
"amdfand",
|
||||
opts.pid_file
|
||||
.unwrap_or_else(|| String::from(DEFAULT_PID_FILE_NAME)),
|
||||
)?;
|
||||
pid_file.acquire()?;
|
||||
let res = service::run(config);
|
||||
pid_file.release()?;
|
||||
res
|
||||
}
|
||||
Some(FanCommand::SetAutomatic(switcher)) => {
|
||||
change_mode::run(switcher, FanMode::Automatic, config)
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
use gumdrop::Options;
|
||||
|
||||
use amdgpu::utils::hw_mons;
|
||||
use amdgpu::{config_reloaded, is_reload_required, listen_unix_signal};
|
||||
use amdgpu_config::fan::Config;
|
||||
|
||||
use crate::command::Fan;
|
||||
use crate::AmdFanError;
|
||||
|
||||
/// Start service which will change fan speed according to config and GPU temperature
|
||||
pub fn run(config: Config) -> crate::Result<()> {
|
||||
pub fn run(mut config: Config) -> crate::Result<()> {
|
||||
listen_unix_signal();
|
||||
|
||||
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
||||
|
||||
if hw_mons.is_empty() {
|
||||
@ -15,6 +18,12 @@ pub fn run(config: Config) -> crate::Result<()> {
|
||||
}
|
||||
let mut cache = std::collections::HashMap::new();
|
||||
loop {
|
||||
if is_reload_required() {
|
||||
log::info!("Reloading config...");
|
||||
config = config.reload()?;
|
||||
log::info!(" config reloaded");
|
||||
config_reloaded();
|
||||
}
|
||||
for hw_mon in hw_mons.iter_mut() {
|
||||
let gpu_temp = config
|
||||
.temp_input()
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "amdgpu-config"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
edition = "2021"
|
||||
description = "Subcomponent of AMDGPU tools"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -16,9 +16,10 @@ path = "./src/lib.rs"
|
||||
fan = []
|
||||
voltage = []
|
||||
monitor = []
|
||||
gui = []
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.8" }
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9" }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
|
@ -3,20 +3,37 @@ use amdgpu::{LogLevel, TempInput};
|
||||
|
||||
pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml";
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct MatrixPoint {
|
||||
pub temp: f64,
|
||||
pub speed: f64,
|
||||
}
|
||||
|
||||
impl MatrixPoint {
|
||||
pub const MIN: MatrixPoint = MatrixPoint {
|
||||
temp: 0.0,
|
||||
speed: 0.0,
|
||||
};
|
||||
pub const MAX: MatrixPoint = MatrixPoint {
|
||||
temp: 100.0,
|
||||
speed: 100.0,
|
||||
};
|
||||
|
||||
pub fn new(temp: f64, speed: f64) -> Self {
|
||||
Self { temp, speed }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
cards: Option<Vec<String>>,
|
||||
log_level: LogLevel,
|
||||
speed_matrix: Vec<MatrixPoint>,
|
||||
#[serde(skip)]
|
||||
path: String,
|
||||
/// One of temperature inputs /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input
|
||||
/// If nothing is provided higher reading will be taken (this is not good!)
|
||||
temp_input: Option<TempInput>,
|
||||
log_level: LogLevel,
|
||||
cards: Option<Vec<String>>,
|
||||
speed_matrix: Vec<MatrixPoint>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -28,6 +45,23 @@ impl Config {
|
||||
self.cards.as_ref()
|
||||
}
|
||||
|
||||
pub fn reload(self) -> Result<Config, ConfigError> {
|
||||
let config = load_config(&self.path)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn speed_matrix(&self) -> &[MatrixPoint] {
|
||||
&self.speed_matrix
|
||||
}
|
||||
|
||||
pub fn speed_matrix_mut(&mut self) -> &mut [MatrixPoint] {
|
||||
&mut self.speed_matrix
|
||||
}
|
||||
|
||||
pub fn speed_matrix_vec_mut(&mut self) -> &mut Vec<MatrixPoint> {
|
||||
&mut self.speed_matrix
|
||||
}
|
||||
|
||||
pub fn speed_matrix_point(&self, temp: f64) -> Option<&MatrixPoint> {
|
||||
match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
|
||||
Some(idx) => self.speed_matrix.get(idx),
|
||||
@ -62,6 +96,10 @@ impl Config {
|
||||
self.temp_input.as_ref()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn min_speed(&self) -> f64 {
|
||||
self.speed_matrix.first().map(|p| p.speed).unwrap_or(0f64)
|
||||
}
|
||||
@ -74,6 +112,7 @@ impl Config {
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: String::from(DEFAULT_FAN_CONFIG_PATH),
|
||||
#[allow(deprecated)]
|
||||
cards: None,
|
||||
log_level: LogLevel::Error,
|
||||
@ -143,7 +182,8 @@ pub enum ConfigError {
|
||||
}
|
||||
|
||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
||||
let config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
||||
let mut config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
||||
config.path = String::from(config_path);
|
||||
|
||||
let mut last_point: Option<&MatrixPoint> = None;
|
||||
|
||||
@ -314,3 +354,20 @@ mod speed_for_temp {
|
||||
assert_eq!(config.speed_for_temp(160f64), 100f64);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serde_tests {
|
||||
use crate::fan::Config;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let res = toml::to_string(&Config::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
|
51
amdgpu-config/src/gui.rs
Normal file
51
amdgpu-config/src/gui.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use amdgpu::utils::ensure_config;
|
||||
use amdgpu::LogLevel;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Config {
|
||||
/// Minimal log level
|
||||
log_level: LogLevel,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: LogLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn log_level(&self) -> LogLevel {
|
||||
self.log_level
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
||||
let config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serde_tests {
|
||||
use crate::gui::Config;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let res = toml::to_string(&Config::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
#[cfg(feature = "fan")]
|
||||
pub mod fan;
|
||||
#[cfg(feature = "gui")]
|
||||
pub mod gui;
|
||||
#[cfg(feature = "monitor")]
|
||||
pub mod monitor;
|
||||
#[cfg(feature = "voltage")]
|
||||
|
@ -51,3 +51,20 @@ pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serde_tests {
|
||||
use crate::monitor::Config;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let res = toml::to_string(&Config::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
|
@ -29,3 +29,20 @@ impl Config {
|
||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
||||
ensure_config::<Config, ConfigError, _>(config_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod serde_tests {
|
||||
use crate::voltage::Config;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let res = toml::to_string(&Config::default());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
}
|
||||
|
7
amdgpu/.cargo/config.toml
Normal file
7
amdgpu/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "amdgpu"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
edition = "2018"
|
||||
description = "Subcomponent of AMDGPU fan control service"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -8,11 +8,20 @@ keywords = ["hardware", "amdgpu"]
|
||||
categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[features]
|
||||
gui-helper = []
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
thiserror = "1.0.30"
|
||||
ron = { version = "0.7.0" }
|
||||
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
pretty_env_logger = { version = "0.4.0" }
|
||||
|
||||
nix = { version = "0.23.1" }
|
||||
|
||||
pidlock = { version = "0.1.4" }
|
||||
|
@ -1,3 +1,30 @@
|
||||
#[cfg(feature = "gui-helper")]
|
||||
use crate::helper_cmd::GuiHelperError;
|
||||
use pidlock::PidlockError;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
||||
pub struct IoFailure {
|
||||
pub io: std::io::Error,
|
||||
pub path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl std::error::Error for IoFailure {}
|
||||
|
||||
impl Debug for IoFailure {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"File system error for {:?}. {:?}",
|
||||
self.path, self.io
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IoFailure {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{:?}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmdGpuError {
|
||||
#[error("Card must starts with `card`.")]
|
||||
@ -10,10 +37,32 @@ pub enum AmdGpuError {
|
||||
InvalidTempInput(String),
|
||||
#[error("Unable to read GPU vendor")]
|
||||
FailedReadVendor,
|
||||
#[error("{0:?}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
Io(#[from] IoFailure),
|
||||
#[error("Card does not have hwmon")]
|
||||
NoAmdHwMon,
|
||||
#[error("{0:?}")]
|
||||
PidFile(#[from] PidLockError),
|
||||
#[cfg(feature = "gui-helper")]
|
||||
#[error("{0:?}")]
|
||||
GuiHelper(#[from] GuiHelperError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PidLockError {
|
||||
#[error("A lock already exists")]
|
||||
LockExists,
|
||||
#[error("An operation was attempted in the wrong state, e.g. releasing before acquiring.")]
|
||||
InvalidState,
|
||||
}
|
||||
|
||||
impl From<pidlock::PidlockError> for PidLockError {
|
||||
fn from(e: PidlockError) -> Self {
|
||||
match e {
|
||||
pidlock::PidlockError::LockExists => PidLockError::LockExists,
|
||||
pidlock::PidlockError::InvalidState => PidLockError::InvalidState,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AmdGpuError {
|
||||
@ -26,7 +75,7 @@ impl PartialEq for AmdGpuError {
|
||||
(InvalidTempInput(a), InvalidTempInput(b)) => a == b,
|
||||
(FailedReadVendor, FailedReadVendor) => true,
|
||||
(NoAmdHwMon, NoAmdHwMon) => true,
|
||||
(Io(a), Io(b)) => a.kind() == b.kind(),
|
||||
(Io(a), Io(b)) => a.io.kind() == b.io.kind(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
68
amdgpu/src/helper_cmd.rs
Normal file
68
amdgpu/src/helper_cmd.rs
Normal file
@ -0,0 +1,68 @@
|
||||
//! AMD GUI helper communication toolkit
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::ops::Deref;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GuiHelperError {
|
||||
#[error("GUI Helper socket file not found. Is service running?")]
|
||||
NoSockFile,
|
||||
#[error("Failed to connect to /var/lib/amdfand/helper.sock. {0}")]
|
||||
UnableToConnect(#[from] std::io::Error),
|
||||
#[error("Failed to service helper command. {0}")]
|
||||
Serialize(#[from] ron::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Pid(pub i32);
|
||||
|
||||
impl Deref for Pid {
|
||||
type Target = i32;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Command {
|
||||
ReloadConfig { pid: Pid },
|
||||
FanServices,
|
||||
SaveFanConfig { path: String, content: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Response {
|
||||
NoOp,
|
||||
Services(Vec<Pid>),
|
||||
ConfigFileSaved,
|
||||
ConfigFileSaveFailed(String),
|
||||
}
|
||||
|
||||
pub fn sock_file() -> std::path::PathBuf {
|
||||
std::path::Path::new("/tmp").join("amdgui-helper.sock")
|
||||
}
|
||||
|
||||
pub fn send_command(cmd: Command) -> crate::Result<Response> {
|
||||
let sock_path = sock_file();
|
||||
|
||||
if !sock_path.exists() {
|
||||
return Err(GuiHelperError::NoSockFile.into());
|
||||
}
|
||||
|
||||
let mut stream = UnixStream::connect(&sock_path).map_err(GuiHelperError::UnableToConnect)?;
|
||||
let s = ron::to_string(&cmd).map_err(GuiHelperError::Serialize)?;
|
||||
if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() {
|
||||
log::info!("Command send");
|
||||
}
|
||||
|
||||
let res: Response = {
|
||||
let mut s = String::with_capacity(100);
|
||||
let _ = stream.read_to_string(&mut s);
|
||||
ron::from_str(&s).map_err(GuiHelperError::Serialize)?
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::{utils, AmdGpuError, Card, ROOT_DIR};
|
||||
use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HwMonName(pub String);
|
||||
@ -100,7 +100,10 @@ fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf {
|
||||
|
||||
pub fn open_hw_mon(card: Card) -> crate::Result<HwMon> {
|
||||
let read_path = hw_mon_dirs_path(&card);
|
||||
let entries = std::fs::read_dir(read_path)?;
|
||||
let entries = std::fs::read_dir(&read_path).map_err(|io| IoFailure {
|
||||
io,
|
||||
path: read_path,
|
||||
})?;
|
||||
let name = entries
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter_map(|entry| {
|
||||
|
@ -6,7 +6,10 @@ pub use temp_input::*;
|
||||
|
||||
mod card;
|
||||
mod error;
|
||||
#[cfg(feature = "gui-helper")]
|
||||
pub mod helper_cmd;
|
||||
pub mod hw_mon;
|
||||
pub mod lock_file;
|
||||
mod temp_input;
|
||||
pub mod utils;
|
||||
|
||||
@ -30,6 +33,38 @@ pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable";
|
||||
// static PULSE_WIDTH_MODULATION_DISABLED: &str = "0";
|
||||
pub static PULSE_WIDTH_MODULATION_AUTO: &str = "2";
|
||||
|
||||
static mut RELOAD_CONFIG: bool = false;
|
||||
|
||||
extern "C" fn sig_reload(_n: i32) {
|
||||
unsafe {
|
||||
RELOAD_CONFIG = true;
|
||||
};
|
||||
}
|
||||
|
||||
/// Listen for SIGHUP signal. This signal is used to reload config
|
||||
pub fn listen_unix_signal() {
|
||||
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
|
||||
unsafe {
|
||||
let handler: SigHandler = SigHandler::Handler(sig_reload);
|
||||
let action = SigAction::new(handler, SaFlags::SA_NOCLDWAIT, SigSet::empty());
|
||||
sigaction(Signal::SIGHUP, &action).expect("Failed to mount action handler");
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if application received SIGHUP and must reload config file
|
||||
#[inline(always)]
|
||||
pub fn is_reload_required() -> bool {
|
||||
unsafe { RELOAD_CONFIG }
|
||||
}
|
||||
|
||||
/// Reset reload config flag
|
||||
#[inline(always)]
|
||||
pub fn config_reloaded() {
|
||||
unsafe {
|
||||
RELOAD_CONFIG = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AmdGpuError>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
||||
|
44
amdgpu/src/lock_file.rs
Normal file
44
amdgpu/src/lock_file.rs
Normal file
@ -0,0 +1,44 @@
|
||||
//! Create lock file and prevent running 2 identical services.
|
||||
//! NOTE: For 2 amdfand services you may just give 2 different names
|
||||
|
||||
use crate::{IoFailure, PidLockError};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct PidLock(pidlock::Pidlock);
|
||||
|
||||
impl PidLock {
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
sub_dir: P,
|
||||
name: String,
|
||||
) -> std::result::Result<Self, crate::error::AmdGpuError> {
|
||||
let pid_dir_path = std::path::Path::new("/var").join("lib").join(sub_dir);
|
||||
let pid_path = {
|
||||
std::fs::create_dir_all(&pid_dir_path).map_err(|io| IoFailure {
|
||||
io,
|
||||
path: pid_dir_path.clone(),
|
||||
})?;
|
||||
pid_dir_path
|
||||
.join(format!("{}.pid", name))
|
||||
.to_str()
|
||||
.map(String::from)
|
||||
.unwrap()
|
||||
};
|
||||
let pid_file = pidlock::Pidlock::new(&pid_path);
|
||||
Ok(Self(pid_file))
|
||||
}
|
||||
|
||||
/// Create new lock file. File will be created if:
|
||||
/// * pid file does not exists
|
||||
/// * pid file exists but process is dead
|
||||
pub fn acquire(&mut self) -> Result<(), crate::error::AmdGpuError> {
|
||||
self.0.acquire().map_err(PidLockError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove lock file
|
||||
/// Remove lock file
|
||||
pub fn release(&mut self) -> Result<(), crate::error::AmdGpuError> {
|
||||
self.0.release().map_err(PidLockError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use crate::AmdGpuError;
|
||||
use serde::Serializer;
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone, serde::Serialize)]
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
pub struct TempInput(pub u16);
|
||||
|
||||
impl TempInput {
|
||||
@ -35,6 +36,15 @@ impl std::str::FromStr for TempInput {
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for TempInput {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for TempInput {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
@ -82,6 +82,8 @@ pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Try to read from config file or create new config file.
|
||||
/// Create only if it does not exists, malformed file will raise error
|
||||
pub fn ensure_config<Config, Error, P>(config_path: P) -> std::result::Result<Config, Error>
|
||||
where
|
||||
Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized,
|
||||
@ -102,6 +104,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan sysfs for sensor files
|
||||
pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
|
||||
let dir = match std::fs::read_dir(hw_mon.mon_dir()) {
|
||||
Ok(d) => d,
|
||||
@ -117,6 +120,7 @@ pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Create config directory if does not exists
|
||||
pub fn ensure_config_dir() -> std::io::Result<()> {
|
||||
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) {
|
||||
std::fs::create_dir_all(CONFIG_DIR)?;
|
||||
|
7
amdgui-helper/.cargo/config
Normal file
7
amdgui-helper/.cargo/config
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
7
amdgui-helper/.cargo/config.toml
Normal file
7
amdgui-helper/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
33
amdgui-helper/Cargo.toml
Normal file
33
amdgui-helper/Cargo.toml
Normal file
@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "amdgui-helper"
|
||||
version = "1.0.9"
|
||||
edition = "2018"
|
||||
description = "AMDGPU fan control service"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["hardware", "amdgpu"]
|
||||
categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
ron = { version = "0.7.0" }
|
||||
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
pretty_env_logger = { version = "0.4.0" }
|
||||
|
||||
nix = { version = "0.23.1" }
|
||||
|
||||
sudo = { version = "0.6.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
7
amdgui-helper/README.md
Normal file
7
amdgui-helper/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# amdgui-helper
|
||||
|
||||
Daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files
|
||||
|
||||
You can communicate with it using sock file `/tmp/amdgui-helper.sock` using helper `Command` from `amdgpu`.
|
||||
|
||||
Each connection is single use and will be terminated after sending `Response`.
|
202
amdgui-helper/src/main.rs
Normal file
202
amdgui-helper/src/main.rs
Normal file
@ -0,0 +1,202 @@
|
||||
//! Special daemon with root privileges. Since GUI should not have (and sometimes can't have) root
|
||||
//! privileges and service processes are designed to be as small as possible this is proxy.
|
||||
//!
|
||||
//! It is responsible for:
|
||||
//! * Loading all amdfand processes. In order to do this process needs to be killed with signal 0 to check if it still is alive
|
||||
//! * Reload amdfand process with signal SIGHUP
|
||||
//! * Save changed config file
|
||||
//!
|
||||
//! It is using `/tmp/amdgui-helper.sock` file and `ron` serialization for communication.
|
||||
//! After each operation connection is terminated so each command needs new connection.
|
||||
#![allow(clippy::non_octal_unix_permissions)]
|
||||
|
||||
use amdgpu::helper_cmd::{Command, Pid, Response};
|
||||
use amdgpu::IoFailure;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::Permissions;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Io(#[from] amdgpu::IoFailure),
|
||||
#[error("{0}")]
|
||||
Lock(#[from] amdgpu::AmdGpuError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "DEBUG");
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
|
||||
let mut lock = amdgpu::lock_file::PidLock::new("amdgui", String::from("helper"))?;
|
||||
lock.acquire()?;
|
||||
|
||||
let sock_path = amdgpu::helper_cmd::sock_file();
|
||||
let listener = {
|
||||
let _ = std::fs::remove_file(&sock_path);
|
||||
|
||||
UnixListener::bind(&sock_path).map_err(|io| IoFailure {
|
||||
io,
|
||||
path: sock_path.clone(),
|
||||
})?
|
||||
};
|
||||
if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0x777)) {
|
||||
log::error!("Failed to change gui helper socket file mode. {:?}", e);
|
||||
}
|
||||
|
||||
while let Ok((stream, _addr)) = listener.accept() {
|
||||
handle_connection(stream);
|
||||
}
|
||||
|
||||
lock.release()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Service(UnixStream);
|
||||
|
||||
impl Service {
|
||||
/// Serialize and send command
|
||||
pub fn write_response(&mut self, res: amdgpu::helper_cmd::Response) {
|
||||
match ron::to_string(&res) {
|
||||
Ok(buffer) => match self.0.write_all(buffer.as_bytes()) {
|
||||
Ok(_) => {
|
||||
log::info!("Response successfully written")
|
||||
}
|
||||
Err(e) => log::warn!("Failed to write response. {:?}", e),
|
||||
},
|
||||
Err(e) => {
|
||||
log::warn!("Failed to serialize response {:?}. {:?}", res, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read from `.sock` file new line separated commands
|
||||
pub fn read_command(&mut self) -> Option<String> {
|
||||
let mut command = String::with_capacity(100);
|
||||
log::info!("Reading stream...");
|
||||
read_line(&mut self.0, &mut command);
|
||||
if command.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(command)
|
||||
}
|
||||
|
||||
/// Close connection with no operation response
|
||||
pub fn kill(mut self) {
|
||||
self.write_response(Response::NoOp);
|
||||
self.close();
|
||||
}
|
||||
|
||||
pub fn close(self) {
|
||||
let _ = self.0.shutdown(Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_connection(stream: UnixStream) {
|
||||
let mut service = Service(stream);
|
||||
|
||||
let command = match service.read_command() {
|
||||
Some(s) => s,
|
||||
_ => return service.kill(),
|
||||
};
|
||||
|
||||
log::info!("Incoming {:?}", command);
|
||||
let cmd = match ron::from_str::<amdgpu::helper_cmd::Command>(command.trim()) {
|
||||
Ok(cmd) => cmd,
|
||||
Err(e) => {
|
||||
log::warn!("Invalid message {:?}. {:?}", command, e);
|
||||
return service.kill();
|
||||
}
|
||||
};
|
||||
handle_command(service, cmd);
|
||||
}
|
||||
|
||||
fn handle_command(mut service: Service, cmd: Command) {
|
||||
match cmd {
|
||||
Command::ReloadConfig { pid } => {
|
||||
log::info!("Reloading config file for pid {:?}", pid);
|
||||
handle_reload_config(service, pid);
|
||||
}
|
||||
Command::FanServices => handle_fan_services(service),
|
||||
Command::SaveFanConfig { path, content } => {
|
||||
handle_save_fan_config(&mut service, path, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_save_fan_config(service: &mut Service, path: String, content: String) {
|
||||
match std::fs::write(path, content) {
|
||||
Err(e) => service.write_response(Response::ConfigFileSaveFailed(format!("{:?}", e))),
|
||||
Ok(..) => service.write_response(Response::ConfigFileSaved),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_fan_services(mut service: Service) {
|
||||
log::info!("Loading fan services");
|
||||
let services = read_fan_services();
|
||||
log::info!("Loaded fan services pid {:?}", services);
|
||||
service.write_response(Response::Services(services));
|
||||
}
|
||||
|
||||
fn read_line(stream: &mut UnixStream, command: &mut String) {
|
||||
let mut buffer = [0];
|
||||
while stream.read_exact(&mut buffer).is_ok() {
|
||||
if buffer[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
match std::str::from_utf8(&buffer) {
|
||||
Ok(s) => {
|
||||
command.push_str(s);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to read from client. {:?}", e);
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_reload_config(service: Service, pid: Pid) {
|
||||
unsafe {
|
||||
nix::libc::kill(pid.0, nix::sys::signal::Signal::SIGHUP as i32);
|
||||
}
|
||||
service.kill();
|
||||
}
|
||||
|
||||
fn read_fan_services() -> Vec<Pid> {
|
||||
if let Ok(entry) = std::fs::read_dir("/var/lib/amdfand") {
|
||||
entry
|
||||
.filter(|e| {
|
||||
e.as_ref()
|
||||
.map(|e| {
|
||||
log::info!("Extension is {:?}", e.path().extension());
|
||||
e.path().extension().and_then(OsStr::to_str) == Some("pid")
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.filter_map(|e| {
|
||||
log::info!("Found entry {:?}", e);
|
||||
match e {
|
||||
Ok(entry) => std::fs::read_to_string(entry.path())
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.filter(|pid| unsafe { nix::libc::kill(*pid, 0) } == 0),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.map(Pid)
|
||||
.collect()
|
||||
} else {
|
||||
log::warn!("Directory /var/lib/amdfand not found");
|
||||
vec![]
|
||||
}
|
||||
}
|
7
amdguid/.cargo/config
Normal file
7
amdguid/.cargo/config
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
7
amdguid/.cargo/config.toml
Normal file
7
amdguid/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-gnu"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
64
amdguid/Cargo.toml
Normal file
64
amdguid/Cargo.toml
Normal file
@ -0,0 +1,64 @@
|
||||
[package]
|
||||
name = "amdguid"
|
||||
version = "1.0.9"
|
||||
edition = "2018"
|
||||
description = "AMDGPU fan control service"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["hardware", "amdgpu"]
|
||||
categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[features]
|
||||
wayland = [
|
||||
"egui_vulkano",
|
||||
"vulkano-win",
|
||||
"vulkano",
|
||||
"vulkano-shaders",
|
||||
"_gui"
|
||||
]
|
||||
xorg = ["glium", "egui_glium", "_gui"]
|
||||
default = ["wayland"]
|
||||
_gui = [
|
||||
"egui",
|
||||
"epaint",
|
||||
"epi",
|
||||
"winit",
|
||||
"egui-winit",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
pretty_env_logger = { version = "0.4.0" }
|
||||
|
||||
egui = { version = "0.15.0", optional = true }
|
||||
epaint = { version = "0.15.0", features = ["serialize"], optional = true }
|
||||
epi = { version = "0.15.0", optional = true }
|
||||
winit = { version = "0.25.0", optional = true }
|
||||
egui-winit = { version = "0.15.0", optional = true }
|
||||
# vulkan
|
||||
egui_vulkano = { version = "0.4.0", optional = true }
|
||||
vulkano-win = { version = "0.25.0", optional = true }
|
||||
vulkano = { version = "0.25.0", optional = true }
|
||||
vulkano-shaders = { version = "0.25.0", optional = true }
|
||||
# xorg
|
||||
glium = { version = "0.30", optional = true }
|
||||
egui_glium = { version = "0.15.0", optional = true }
|
||||
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
parking_lot = { version = "0.11.2" }
|
||||
|
||||
nix = { version = "0.23.1" }
|
||||
|
||||
[dev-dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
15
amdguid/README.md
Normal file
15
amdguid/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# AMD GPU gui tool
|
||||
|
||||
Provides basic FAN configuration.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* amdvold config manipulation
|
||||
* Fix Drag & drop functionality - mouse is not followed properly
|
||||
* Program profiles
|
||||
|
||||
## Screenshots
|
||||
|
||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png)
|
||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png)
|
||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png)
|
151
amdguid/src/app.rs
Normal file
151
amdguid/src/app.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use crate::widgets::{ChangeFanSettings, CoolingPerformance};
|
||||
use amdgpu::helper_cmd::Pid;
|
||||
use egui::{CtxRef, Ui};
|
||||
use epi::Frame;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum ChangeState {
|
||||
New,
|
||||
Reloading,
|
||||
Success,
|
||||
Failure(String),
|
||||
}
|
||||
|
||||
impl Default for ChangeState {
|
||||
fn default() -> Self {
|
||||
ChangeState::New
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FanService {
|
||||
pub pid: Pid,
|
||||
pub reload: ChangeState,
|
||||
}
|
||||
|
||||
impl FanService {
|
||||
pub fn new(pid: Pid) -> FanService {
|
||||
Self {
|
||||
pid,
|
||||
reload: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FanServices(pub Vec<FanService>);
|
||||
|
||||
impl FanServices {
|
||||
pub fn list_changed(&self, other: &[Pid]) -> bool {
|
||||
if self.0.len() != other.len() {
|
||||
return true;
|
||||
}
|
||||
let c = self
|
||||
.0
|
||||
.iter()
|
||||
.fold(HashMap::with_capacity(other.len()), |mut h, service| {
|
||||
h.insert(service.pid.0, true);
|
||||
h
|
||||
});
|
||||
!other.iter().all(|s| c.contains_key(&s.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Pid>> for FanServices {
|
||||
fn from(v: Vec<Pid>) -> Self {
|
||||
Self(v.into_iter().map(FanService::new).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Page {
|
||||
Config,
|
||||
Monitoring,
|
||||
Settings,
|
||||
}
|
||||
|
||||
impl Default for Page {
|
||||
fn default() -> Self {
|
||||
Self::Config
|
||||
}
|
||||
}
|
||||
|
||||
pub type FanConfig = Arc<Mutex<amdgpu_config::fan::Config>>;
|
||||
|
||||
static RELOAD_PID_LIST_DELAY: u8 = 18;
|
||||
|
||||
pub struct StatefulConfig {
|
||||
pub config: FanConfig,
|
||||
pub state: ChangeState,
|
||||
}
|
||||
|
||||
pub struct AmdGui {
|
||||
pub page: Page,
|
||||
pid_files: FanServices,
|
||||
cooling_performance: CoolingPerformance,
|
||||
change_fan_settings: ChangeFanSettings,
|
||||
config: StatefulConfig,
|
||||
reload_pid_list_delay: u8,
|
||||
}
|
||||
|
||||
impl epi::App for AmdGui {
|
||||
fn update(&mut self, _ctx: &CtxRef, _frame: &mut Frame<'_>) {}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"AMD GUI"
|
||||
}
|
||||
}
|
||||
|
||||
impl AmdGui {
|
||||
pub fn new_with_config(config: FanConfig) -> Self {
|
||||
Self {
|
||||
page: Default::default(),
|
||||
pid_files: FanServices::from(vec![]),
|
||||
cooling_performance: CoolingPerformance::new(100, config.clone()),
|
||||
change_fan_settings: ChangeFanSettings::new(config.clone()),
|
||||
config: StatefulConfig {
|
||||
config,
|
||||
state: ChangeState::New,
|
||||
},
|
||||
reload_pid_list_delay: RELOAD_PID_LIST_DELAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut Ui) {
|
||||
match self.page {
|
||||
Page::Config => {
|
||||
self.change_fan_settings
|
||||
.draw(ui, &mut self.pid_files, &mut self.config);
|
||||
}
|
||||
Page::Monitoring => {
|
||||
self.cooling_performance.draw(ui, &self.pid_files);
|
||||
}
|
||||
Page::Settings => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
self.cooling_performance.tick();
|
||||
if self.pid_files.0.is_empty() || self.reload_pid_list_delay.checked_sub(1).is_none() {
|
||||
self.reload_pid_list_delay = RELOAD_PID_LIST_DELAY;
|
||||
match amdgpu::helper_cmd::send_command(amdgpu::helper_cmd::Command::FanServices) {
|
||||
Ok(amdgpu::helper_cmd::Response::Services(services))
|
||||
if self.pid_files.list_changed(&services) =>
|
||||
{
|
||||
self.pid_files = FanServices::from(services);
|
||||
}
|
||||
Ok(amdgpu::helper_cmd::Response::Services(_services)) => {
|
||||
// SKIP
|
||||
}
|
||||
Ok(res) => {
|
||||
log::warn!("Unexpected response {:?} while loading fan services", res);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load amd fan services pid list. {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.reload_pid_list_delay -= 1;
|
||||
}
|
||||
}
|
||||
}
|
10
amdguid/src/backend/mod.rs
Normal file
10
amdguid/src/backend/mod.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[cfg(feature = "wayland")]
|
||||
pub mod wayland;
|
||||
#[cfg(feature = "xorg")]
|
||||
pub mod xorg;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use wayland::run_app;
|
||||
|
||||
#[cfg(feature = "xorg")]
|
||||
pub use xorg::run_app;
|
393
amdguid/src/backend/wayland.rs
Normal file
393
amdguid/src/backend/wayland.rs
Normal file
@ -0,0 +1,393 @@
|
||||
use crate::app::{AmdGui, Page};
|
||||
use egui::panel::TopBottomSide;
|
||||
use egui::{Layout, PointerButton};
|
||||
use epaint::TextStyle;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer};
|
||||
use vulkano::command_buffer::{
|
||||
AutoCommandBufferBuilder, CommandBufferUsage, DynamicState, SubpassContents,
|
||||
};
|
||||
use vulkano::format::Format;
|
||||
use vulkano::image::view::ImageView;
|
||||
use vulkano::image::{ImageUsage, SwapchainImage};
|
||||
use vulkano::render_pass::{Framebuffer, FramebufferAbstract, RenderPass, Subpass};
|
||||
use vulkano::swapchain::{AcquireError, ColorSpace, Swapchain, SwapchainCreationError};
|
||||
use vulkano::sync::{FlushError, GpuFuture};
|
||||
use vulkano::{swapchain, sync, Version};
|
||||
use vulkano_win::VkSurfaceBuild;
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{Event, WindowEvent};
|
||||
use winit::event_loop::ControlFlow;
|
||||
use winit::window::Window;
|
||||
|
||||
pub mod vs {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "vertex",
|
||||
src: "
|
||||
#version 450
|
||||
layout(location = 0) in vec2 position;
|
||||
void main() {
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
"
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fs {
|
||||
vulkano_shaders::shader! {
|
||||
ty: "fragment",
|
||||
src: "
|
||||
#version 450
|
||||
layout(location = 0) out vec4 f_color;
|
||||
void main() {
|
||||
f_color = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
struct Vertex {
|
||||
position: [f32; 2],
|
||||
}
|
||||
|
||||
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>) {
|
||||
let required_extensions = vulkano_win::required_extensions();
|
||||
let instance =
|
||||
vulkano::instance::Instance::new(None, Version::V1_0, &required_extensions, None).unwrap();
|
||||
let physical = vulkano::device::physical::PhysicalDevice::enumerate(&instance)
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let event_loop = winit::event_loop::EventLoop::new();
|
||||
let surface = winit::window::WindowBuilder::new()
|
||||
.with_inner_size(PhysicalSize::new(1024, 768))
|
||||
.with_title("AMD GUI")
|
||||
.build_vk_surface(&event_loop, instance.clone())
|
||||
.unwrap();
|
||||
|
||||
// vulkan
|
||||
let queue_family = physical
|
||||
.queue_families()
|
||||
.find(|&q| q.supports_graphics() && surface.is_supported(q).unwrap_or(false))
|
||||
.unwrap();
|
||||
|
||||
let device_ext = vulkano::device::DeviceExtensions {
|
||||
khr_swapchain: true,
|
||||
..vulkano::device::DeviceExtensions::none()
|
||||
};
|
||||
let (device, mut queues) = vulkano::device::Device::new(
|
||||
physical,
|
||||
physical.supported_features(),
|
||||
&device_ext,
|
||||
[(queue_family, 0.5)].iter().cloned(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let queue = queues.next().unwrap();
|
||||
|
||||
let (mut swapchain, images) = {
|
||||
let caps = surface.capabilities(physical).unwrap();
|
||||
let alpha = caps.supported_composite_alpha.iter().next().unwrap();
|
||||
|
||||
assert!(&caps
|
||||
.supported_formats
|
||||
.contains(&(Format::B8G8R8A8Srgb, ColorSpace::SrgbNonLinear)));
|
||||
let format = Format::B8G8R8A8Srgb;
|
||||
let dimensions: [u32; 2] = surface.window().inner_size().into();
|
||||
|
||||
Swapchain::start(device.clone(), surface.clone())
|
||||
.num_images(caps.min_image_count)
|
||||
.format(format)
|
||||
.dimensions(dimensions)
|
||||
.usage(ImageUsage::color_attachment())
|
||||
.sharing_mode(&queue)
|
||||
.composite_alpha(alpha)
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
vulkano::impl_vertex!(Vertex, position);
|
||||
|
||||
let vertex_buffer = {
|
||||
CpuAccessibleBuffer::from_iter(
|
||||
device.clone(),
|
||||
BufferUsage::all(),
|
||||
false,
|
||||
[
|
||||
Vertex {
|
||||
position: [-0.5, -0.25],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.0, 0.5],
|
||||
},
|
||||
Vertex {
|
||||
position: [0.25, -0.1],
|
||||
},
|
||||
]
|
||||
.iter()
|
||||
.cloned(),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let vs = vs::Shader::load(device.clone()).unwrap();
|
||||
let fs = fs::Shader::load(device.clone()).unwrap();
|
||||
|
||||
let render_pass = Arc::new(
|
||||
vulkano::ordered_passes_renderpass!(
|
||||
device.clone(),
|
||||
attachments: {
|
||||
color: {
|
||||
load: Clear,
|
||||
store: Store,
|
||||
format: swapchain.format(),
|
||||
samples: 1,
|
||||
}
|
||||
},
|
||||
passes: [
|
||||
{ color: [color], depth_stencil: {}, input: [] },
|
||||
{ color: [color], depth_stencil: {}, input: [] } // Create a second render pass to draw egui
|
||||
]
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let pipeline = Arc::new(
|
||||
vulkano::pipeline::GraphicsPipeline::start()
|
||||
.vertex_input_single_buffer::<Vertex>()
|
||||
.vertex_shader(vs.main_entry_point(), ())
|
||||
.triangle_list()
|
||||
.viewports_dynamic_scissors_irrelevant(1)
|
||||
.fragment_shader(fs.main_entry_point(), ())
|
||||
.render_pass(Subpass::from(render_pass.clone(), 0).unwrap())
|
||||
.build(device.clone())
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let mut dynamic_state = DynamicState {
|
||||
line_width: None,
|
||||
viewports: None,
|
||||
scissors: None,
|
||||
compare_mask: None,
|
||||
write_mask: None,
|
||||
reference: None,
|
||||
};
|
||||
|
||||
let mut framebuffers =
|
||||
window_size_dependent_setup(&images, render_pass.clone(), &mut dynamic_state);
|
||||
|
||||
let mut recreate_swap_chain = false;
|
||||
|
||||
let mut previous_frame_end = Some(sync::now(device.clone()).boxed());
|
||||
|
||||
let window = surface.window();
|
||||
let mut egui_ctx = egui::CtxRef::default();
|
||||
let mut egui_winit = egui_winit::State::new(window);
|
||||
|
||||
let mut egui_painter = egui_vulkano::Painter::new(
|
||||
device.clone(),
|
||||
queue.clone(),
|
||||
Subpass::from(render_pass.clone(), 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
match event {
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
Event::WindowEvent {
|
||||
event: WindowEvent::Resized(_),
|
||||
..
|
||||
} => {
|
||||
recreate_swap_chain = true;
|
||||
}
|
||||
Event::WindowEvent { event, .. } => {
|
||||
let egui_consumed_event = egui_winit.on_event(&egui_ctx, &event);
|
||||
if !egui_consumed_event {
|
||||
// do your own event handling here
|
||||
};
|
||||
}
|
||||
Event::RedrawEventsCleared => {
|
||||
previous_frame_end.as_mut().unwrap().cleanup_finished();
|
||||
|
||||
if recreate_swap_chain {
|
||||
let dimensions: [u32; 2] = surface.window().inner_size().into();
|
||||
let (new_swap_chain, new_images) =
|
||||
match swapchain.recreate().dimensions(dimensions).build() {
|
||||
Ok(r) => r,
|
||||
Err(SwapchainCreationError::UnsupportedDimensions) => return,
|
||||
Err(e) => panic!("Failed to recreate swap chain: {:?}", e),
|
||||
};
|
||||
|
||||
swapchain = new_swap_chain;
|
||||
framebuffers = window_size_dependent_setup(
|
||||
&new_images,
|
||||
render_pass.clone(),
|
||||
&mut dynamic_state,
|
||||
);
|
||||
recreate_swap_chain = false;
|
||||
}
|
||||
|
||||
let (image_num, suboptimal, acquire_future) =
|
||||
match swapchain::acquire_next_image(swapchain.clone(), None) {
|
||||
Ok(r) => r,
|
||||
Err(AcquireError::OutOfDate) => {
|
||||
recreate_swap_chain = true;
|
||||
return;
|
||||
}
|
||||
Err(e) => panic!("Failed to acquire next image: {:?}", e),
|
||||
};
|
||||
|
||||
if suboptimal {
|
||||
recreate_swap_chain = true;
|
||||
}
|
||||
|
||||
let clear_values = vec![[0.0, 0.0, 1.0, 1.0].into()];
|
||||
let mut builder = AutoCommandBufferBuilder::primary(
|
||||
device.clone(),
|
||||
queue.family(),
|
||||
CommandBufferUsage::OneTimeSubmit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Do your usual rendering
|
||||
builder
|
||||
.begin_render_pass(
|
||||
framebuffers[image_num].clone(),
|
||||
SubpassContents::Inline,
|
||||
clear_values,
|
||||
)
|
||||
.unwrap()
|
||||
.draw(
|
||||
pipeline.clone(),
|
||||
&dynamic_state,
|
||||
vertex_buffer.clone(),
|
||||
(),
|
||||
(),
|
||||
)
|
||||
.unwrap(); // Don't end the render pass yet
|
||||
|
||||
egui_ctx.begin_frame(egui_winit.take_egui_input(surface.window()));
|
||||
|
||||
egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show(
|
||||
&egui_ctx,
|
||||
|ui| {
|
||||
let mut child =
|
||||
ui.child_ui(ui.available_rect_before_wrap(), Layout::left_to_right());
|
||||
if child
|
||||
.add(egui::Button::new("Config").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Config;
|
||||
}
|
||||
if child
|
||||
.add(egui::Button::new("Monitoring").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Monitoring;
|
||||
}
|
||||
if child
|
||||
.add(egui::Button::new("Settings").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Settings;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
egui::containers::CentralPanel::default().show(&egui_ctx, |ui| {
|
||||
let mut gui = amd_gui.lock();
|
||||
let page = gui.page;
|
||||
match page {
|
||||
Page::Config => {
|
||||
gui.ui(ui);
|
||||
}
|
||||
Page::Monitoring => {
|
||||
gui.ui(ui);
|
||||
}
|
||||
Page::Settings => {
|
||||
egui_ctx.settings_ui(ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (egui_output, clipped_shapes) = egui_ctx.end_frame();
|
||||
egui_winit.handle_output(surface.window(), &egui_ctx, egui_output);
|
||||
let size = surface.window().inner_size();
|
||||
egui_painter
|
||||
.draw(
|
||||
&mut builder,
|
||||
&dynamic_state,
|
||||
[size.width as f32, size.height as f32],
|
||||
&egui_ctx,
|
||||
clipped_shapes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// End the render pass as usual
|
||||
builder.end_render_pass().unwrap();
|
||||
|
||||
let command_buffer = builder.build().unwrap();
|
||||
|
||||
let future = previous_frame_end
|
||||
.take()
|
||||
.unwrap()
|
||||
.join(acquire_future)
|
||||
.then_execute(queue.clone(), command_buffer)
|
||||
.unwrap()
|
||||
.then_swapchain_present(queue.clone(), swapchain.clone(), image_num)
|
||||
.then_signal_fence_and_flush();
|
||||
|
||||
match future {
|
||||
Ok(future) => {
|
||||
previous_frame_end = Some(future.boxed());
|
||||
}
|
||||
Err(FlushError::OutOfDate) => {
|
||||
recreate_swap_chain = true;
|
||||
previous_frame_end = Some(sync::now(device.clone()).boxed());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to flush future: {:?}", e);
|
||||
previous_frame_end = Some(sync::now(device.clone()).boxed());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn window_size_dependent_setup(
|
||||
images: &[Arc<SwapchainImage<Window>>],
|
||||
render_pass: Arc<RenderPass>,
|
||||
dynamic_state: &mut DynamicState,
|
||||
) -> Vec<Arc<dyn FramebufferAbstract + Send + Sync>> {
|
||||
let dimensions = images[0].dimensions();
|
||||
|
||||
let viewport = vulkano::pipeline::viewport::Viewport {
|
||||
origin: [0.0, 0.0],
|
||||
dimensions: [dimensions[0] as f32, dimensions[1] as f32],
|
||||
depth_range: 0.0..1.0,
|
||||
};
|
||||
dynamic_state.viewports = Some(vec![viewport]);
|
||||
|
||||
images
|
||||
.iter()
|
||||
.map(|image| {
|
||||
let view = ImageView::new(image.clone()).unwrap();
|
||||
Arc::new(
|
||||
Framebuffer::start(render_pass.clone())
|
||||
.add(view)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap(),
|
||||
) as Arc<dyn FramebufferAbstract + Send + Sync>
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
115
amdguid/src/backend/xorg.rs
Normal file
115
amdguid/src/backend/xorg.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use egui::panel::TopBottomSide;
|
||||
use egui::{Layout, PointerButton};
|
||||
use epaint::TextStyle;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app::{AmdGui, Page};
|
||||
use glium::glutin;
|
||||
|
||||
fn create_display(event_loop: &glutin::event_loop::EventLoop<()>) -> glium::Display {
|
||||
let window_builder = glutin::window::WindowBuilder::new()
|
||||
.with_resizable(true)
|
||||
.with_inner_size(glutin::dpi::LogicalSize {
|
||||
width: 800.0,
|
||||
height: 600.0,
|
||||
})
|
||||
.with_title("AMD GUI");
|
||||
|
||||
let context_builder = glutin::ContextBuilder::new()
|
||||
.with_depth_buffer(0)
|
||||
.with_srgb(true)
|
||||
.with_stencil_buffer(0)
|
||||
.with_vsync(true);
|
||||
|
||||
glium::Display::new(window_builder, context_builder, event_loop).unwrap()
|
||||
}
|
||||
|
||||
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>) {
|
||||
let event_loop = glutin::event_loop::EventLoop::with_user_event();
|
||||
let display = create_display(&event_loop);
|
||||
|
||||
let mut egui = egui_glium::EguiGlium::new(&display);
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
let mut redraw = || {
|
||||
egui.begin_frame(&display);
|
||||
|
||||
egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show(
|
||||
egui.ctx(),
|
||||
|ui| {
|
||||
let mut child =
|
||||
ui.child_ui(ui.available_rect_before_wrap(), Layout::left_to_right());
|
||||
if child
|
||||
.add(egui::Button::new("Config").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Config;
|
||||
}
|
||||
if child
|
||||
.add(egui::Button::new("Monitoring").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Monitoring;
|
||||
}
|
||||
if child
|
||||
.add(egui::Button::new("Settings").text_style(TextStyle::Heading))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
amd_gui.lock().page = Page::Settings;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
egui::containers::CentralPanel::default().show(egui.ctx(), |ui| {
|
||||
let mut gui = amd_gui.lock();
|
||||
let page = gui.page;
|
||||
match page {
|
||||
Page::Config => {
|
||||
gui.ui(ui);
|
||||
}
|
||||
Page::Monitoring => {
|
||||
gui.ui(ui);
|
||||
}
|
||||
Page::Settings => {
|
||||
egui.ctx().settings_ui(ui);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (needs_repaint, shapes) = egui.end_frame(&display);
|
||||
|
||||
*control_flow = if needs_repaint {
|
||||
display.gl_window().window().request_redraw();
|
||||
glutin::event_loop::ControlFlow::Poll
|
||||
} else {
|
||||
glutin::event_loop::ControlFlow::Wait
|
||||
};
|
||||
|
||||
{
|
||||
use glium::Surface as _;
|
||||
let mut target = display.draw();
|
||||
|
||||
let color = egui::Rgba::from_rgb(0.1, 0.3, 0.2);
|
||||
target.clear_color(color[0], color[1], color[2], color[3]);
|
||||
egui.paint(&display, &mut target, shapes);
|
||||
target.finish().unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
match event {
|
||||
glutin::event::Event::RedrawRequested(_) => redraw(),
|
||||
glutin::event::Event::WindowEvent { event, .. } => {
|
||||
if egui.is_quit_event(&event) {
|
||||
*control_flow = glium::glutin::event_loop::ControlFlow::Exit;
|
||||
}
|
||||
|
||||
egui.on_event(&event);
|
||||
|
||||
display.gl_window().window().request_redraw();
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
});
|
||||
}
|
147
amdguid/src/items.rs
Normal file
147
amdguid/src/items.rs
Normal file
@ -0,0 +1,147 @@
|
||||
//! Contains items that can be added to a plot.
|
||||
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use egui::Pos2;
|
||||
use epaint::{Color32, Shape, Stroke};
|
||||
|
||||
pub use arrows::*;
|
||||
pub use h_line::*;
|
||||
pub use line::*;
|
||||
pub use marker_shape::*;
|
||||
pub use plot_image::*;
|
||||
pub use plot_item::*;
|
||||
pub use points::*;
|
||||
pub use polygons::*;
|
||||
pub use text::*;
|
||||
pub use v_line::*;
|
||||
pub use value::Value;
|
||||
pub use values::Values;
|
||||
|
||||
mod arrows;
|
||||
mod h_line;
|
||||
mod line;
|
||||
mod marker_shape;
|
||||
mod plot_image;
|
||||
mod plot_item;
|
||||
mod points;
|
||||
mod polygons;
|
||||
mod text;
|
||||
mod v_line;
|
||||
mod value;
|
||||
mod values;
|
||||
|
||||
const DEFAULT_FILL_ALPHA: f32 = 0.05;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum LineStyle {
|
||||
Solid,
|
||||
Dotted { spacing: f32 },
|
||||
Dashed { length: f32 },
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
pub fn dashed_loose() -> Self {
|
||||
Self::Dashed { length: 10.0 }
|
||||
}
|
||||
|
||||
pub fn dashed_dense() -> Self {
|
||||
Self::Dashed { length: 5.0 }
|
||||
}
|
||||
|
||||
pub fn dotted_loose() -> Self {
|
||||
Self::Dotted { spacing: 10.0 }
|
||||
}
|
||||
|
||||
pub fn dotted_dense() -> Self {
|
||||
Self::Dotted { spacing: 5.0 }
|
||||
}
|
||||
|
||||
fn style_line(
|
||||
&self,
|
||||
line: Vec<Pos2>,
|
||||
mut stroke: Stroke,
|
||||
highlight: bool,
|
||||
shapes: &mut Vec<Shape>,
|
||||
) {
|
||||
match line.len() {
|
||||
0 => {}
|
||||
1 => {
|
||||
let mut radius = stroke.width / 2.0;
|
||||
if highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
}
|
||||
shapes.push(Shape::circle_filled(line[0], radius, stroke.color));
|
||||
}
|
||||
_ => {
|
||||
match self {
|
||||
LineStyle::Solid => {
|
||||
if highlight {
|
||||
stroke.width *= 2.0;
|
||||
}
|
||||
for point in line.iter() {
|
||||
shapes.push(Shape::circle_filled(
|
||||
*point,
|
||||
stroke.width * 3.0,
|
||||
Color32::DARK_BLUE,
|
||||
));
|
||||
}
|
||||
shapes.push(Shape::line(line, stroke));
|
||||
}
|
||||
LineStyle::Dotted { spacing } => {
|
||||
let mut radius = stroke.width;
|
||||
if highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
}
|
||||
shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius));
|
||||
}
|
||||
LineStyle::Dashed { length } => {
|
||||
if highlight {
|
||||
stroke.width *= 2.0;
|
||||
}
|
||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
||||
shapes.extend(Shape::dashed_line(
|
||||
&line,
|
||||
stroke,
|
||||
*length,
|
||||
length * golden_ratio,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for LineStyle {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
LineStyle::Solid => "Solid".into(),
|
||||
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
|
||||
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Describes a function y = f(x) with an optional range for x and a number of points.
|
||||
pub struct ExplicitGenerator {
|
||||
function: Box<dyn Fn(f64) -> f64>,
|
||||
x_range: RangeInclusive<f64>,
|
||||
points: usize,
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and
|
||||
/// a horizontal line at the given y-coordinate.
|
||||
#[inline(always)]
|
||||
pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
|
||||
((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y))
|
||||
.then(|| ((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
|
||||
}
|
122
amdguid/src/items/arrows.rs
Normal file
122
amdguid/src/items/arrows.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::values::Values;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::Ui;
|
||||
use epaint::{Color32, Shape};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// A set of arrows.
|
||||
pub struct Arrows {
|
||||
pub(crate) origins: Values,
|
||||
pub(crate) tips: Values,
|
||||
pub(crate) color: Color32,
|
||||
pub(crate) name: String,
|
||||
pub(crate) highlight: bool,
|
||||
}
|
||||
|
||||
impl Arrows {
|
||||
pub fn new(origins: Values, tips: Values) -> Self {
|
||||
Self {
|
||||
origins,
|
||||
tips,
|
||||
color: Color32::TRANSPARENT,
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight these arrows in the plot.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the arrows' color.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this set of arrows.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for Arrows {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
use egui::emath::*;
|
||||
use epaint::Stroke;
|
||||
let Self {
|
||||
origins,
|
||||
tips,
|
||||
color,
|
||||
highlight,
|
||||
..
|
||||
} = self;
|
||||
let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color);
|
||||
origins
|
||||
.values
|
||||
.iter()
|
||||
.zip(tips.values.iter())
|
||||
.map(|(origin, tip)| {
|
||||
(
|
||||
transform.position_from_value(origin),
|
||||
transform.position_from_value(tip),
|
||||
)
|
||||
})
|
||||
.for_each(|(origin, tip)| {
|
||||
let vector = tip - origin;
|
||||
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
|
||||
let tip_length = vector.length() / 4.0;
|
||||
let tip = origin + vector;
|
||||
let dir = vector.normalized();
|
||||
shapes.push(Shape::line_segment([origin, tip], stroke));
|
||||
shapes.push(Shape::line(
|
||||
vec![
|
||||
tip - tip_length * (rot.inverse() * dir),
|
||||
tip,
|
||||
tip - tip_length * (rot * dir),
|
||||
],
|
||||
stroke,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
|
||||
self.origins
|
||||
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
|
||||
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
Some(&self.origins)
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
self.origins.get_bounds()
|
||||
}
|
||||
}
|
118
amdguid/src/items/h_line.rs
Normal file
118
amdguid/src/items/h_line.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::items::LineStyle;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::Ui;
|
||||
use epaint::{Color32, Shape, Stroke};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// A horizontal line in a plot, filling the full width
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HLine {
|
||||
pub(crate) y: f64,
|
||||
pub(crate) stroke: Stroke,
|
||||
pub(crate) name: String,
|
||||
pub(crate) highlight: bool,
|
||||
pub(crate) style: LineStyle,
|
||||
}
|
||||
|
||||
impl HLine {
|
||||
pub fn new(y: impl Into<f64>) -> Self {
|
||||
Self {
|
||||
y: y.into(),
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
name: String::default(),
|
||||
highlight: false,
|
||||
style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this line in the plot by scaling up the line.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a stroke.
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke width. A high value means the plot thickens.
|
||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
||||
self.stroke.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.stroke.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the line's style. Default is `LineStyle::Solid`.
|
||||
pub fn style(mut self, style: LineStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this horizontal line.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for HLine {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let HLine {
|
||||
y,
|
||||
stroke,
|
||||
highlight,
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
let points = vec![
|
||||
transform.position_from_value(&Value::new(transform.bounds().min[0], *y)),
|
||||
transform.position_from_value(&Value::new(transform.bounds().max[0], *y)),
|
||||
];
|
||||
style.style_line(points, *stroke, *highlight, shapes);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.stroke.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
let mut bounds = Bounds::NOTHING;
|
||||
bounds.min[1] = self.y;
|
||||
bounds.max[1] = self.y;
|
||||
bounds
|
||||
}
|
||||
}
|
170
amdguid/src/items/line.rs
Normal file
170
amdguid/src/items/line.rs
Normal file
@ -0,0 +1,170 @@
|
||||
use crate::items;
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::items::{LineStyle, DEFAULT_FILL_ALPHA};
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::{pos2, NumExt, Ui};
|
||||
use epaint::{Color32, Mesh, Rgba, Shape, Stroke};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
impl PlotItem for Line {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let Self {
|
||||
series,
|
||||
stroke,
|
||||
highlight,
|
||||
mut fill,
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let values_tf: Vec<_> = series
|
||||
.values
|
||||
.iter()
|
||||
.map(|v| transform.position_from_value(v))
|
||||
.collect();
|
||||
let n_values = values_tf.len();
|
||||
|
||||
// Fill the area between the line and a reference line, if required.
|
||||
if n_values < 2 {
|
||||
fill = None;
|
||||
}
|
||||
if let Some(y_reference) = fill {
|
||||
let mut fill_alpha = DEFAULT_FILL_ALPHA;
|
||||
if *highlight {
|
||||
fill_alpha = (2.0 * fill_alpha).at_most(1.0);
|
||||
}
|
||||
let y = transform
|
||||
.position_from_value(&Value::new(0.0, y_reference))
|
||||
.y;
|
||||
let fill_color = Rgba::from(stroke.color)
|
||||
.to_opaque()
|
||||
.multiply(fill_alpha)
|
||||
.into();
|
||||
let mut mesh = Mesh::default();
|
||||
let expected_intersections = 20;
|
||||
mesh.reserve_triangles((n_values - 1) * 2);
|
||||
mesh.reserve_vertices(n_values * 2 + expected_intersections);
|
||||
values_tf[0..n_values - 1].windows(2).for_each(|w| {
|
||||
let i = mesh.vertices.len() as u32;
|
||||
mesh.colored_vertex(w[0], fill_color);
|
||||
mesh.colored_vertex(pos2(w[0].x, y), fill_color);
|
||||
if let Some(x) = items::y_intersection(&w[0], &w[1], y) {
|
||||
let point = pos2(x, y);
|
||||
mesh.colored_vertex(point, fill_color);
|
||||
mesh.add_triangle(i, i + 1, i + 2);
|
||||
mesh.add_triangle(i + 2, i + 3, i + 4);
|
||||
} else {
|
||||
mesh.add_triangle(i, i + 1, i + 2);
|
||||
mesh.add_triangle(i + 1, i + 2, i + 3);
|
||||
}
|
||||
});
|
||||
let last = values_tf[n_values - 1];
|
||||
mesh.colored_vertex(last, fill_color);
|
||||
mesh.colored_vertex(pos2(last.x, y), fill_color);
|
||||
shapes.push(Shape::Mesh(mesh));
|
||||
}
|
||||
|
||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
||||
self.series.generate_points(x_range);
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.stroke.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
Some(&self.series)
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
self.series.get_bounds()
|
||||
}
|
||||
}
|
||||
|
||||
/// A series of values forming a path.
|
||||
pub struct Line {
|
||||
pub series: Values,
|
||||
pub stroke: Stroke,
|
||||
pub name: String,
|
||||
pub highlight: bool,
|
||||
pub fill: Option<f32>,
|
||||
pub style: LineStyle,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn new(series: Values) -> Self {
|
||||
Self {
|
||||
series,
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
fill: None,
|
||||
style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this line in the plot by scaling up the line.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a stroke.
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke width. A high value means the plot thickens.
|
||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
||||
self.stroke.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.stroke.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill the area between this line and a given horizontal reference line.
|
||||
pub fn fill(mut self, y_reference: impl Into<f32>) -> Self {
|
||||
self.fill = Some(y_reference.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the line's style. Default is `LineStyle::Solid`.
|
||||
pub fn style(mut self, style: LineStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this line.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
33
amdguid/src/items/marker_shape.rs
Normal file
33
amdguid/src/items/marker_shape.rs
Normal file
@ -0,0 +1,33 @@
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum MarkerShape {
|
||||
Circle,
|
||||
Diamond,
|
||||
Square,
|
||||
Cross,
|
||||
Plus,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Asterisk,
|
||||
}
|
||||
|
||||
impl MarkerShape {
|
||||
/// Get a vector containing all marker shapes.
|
||||
pub fn all() -> impl Iterator<Item = MarkerShape> {
|
||||
[
|
||||
Self::Circle,
|
||||
Self::Diamond,
|
||||
Self::Square,
|
||||
Self::Cross,
|
||||
Self::Plus,
|
||||
Self::Up,
|
||||
Self::Down,
|
||||
Self::Left,
|
||||
Self::Right,
|
||||
Self::Asterisk,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
148
amdguid/src/items/plot_image.rs
Normal file
148
amdguid/src/items/plot_image.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::{pos2, Image, Rect, Ui, Vec2};
|
||||
use epaint::{Color32, Shape, Stroke, TextureId};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// An image in the plot.
|
||||
pub struct PlotImage {
|
||||
pub position: Value,
|
||||
pub texture_id: TextureId,
|
||||
pub uv: Rect,
|
||||
pub size: Vec2,
|
||||
pub bg_fill: Color32,
|
||||
pub tint: Color32,
|
||||
pub highlight: bool,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl PlotImage {
|
||||
/// Create a new image with position and size in plot coordinates.
|
||||
pub fn new(texture_id: TextureId, position: Value, size: impl Into<Vec2>) -> Self {
|
||||
Self {
|
||||
position,
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
texture_id,
|
||||
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
||||
size: size.into(),
|
||||
bg_fill: Default::default(),
|
||||
tint: Color32::WHITE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this image in the plot.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
||||
self.uv = uv.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// A solid color to put behind the image. Useful for transparent images.
|
||||
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
||||
self.bg_fill = bg_fill.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Multiply image color with this. Default is WHITE (no tint).
|
||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
||||
self.tint = tint.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this image.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for PlotImage {
|
||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let Self {
|
||||
position,
|
||||
texture_id,
|
||||
uv,
|
||||
size,
|
||||
bg_fill,
|
||||
tint,
|
||||
highlight,
|
||||
..
|
||||
} = self;
|
||||
let rect = {
|
||||
let left_top = Value::new(
|
||||
position.x as f32 - size.x / 2.0,
|
||||
position.y as f32 - size.y / 2.0,
|
||||
);
|
||||
let right_bottom = Value::new(
|
||||
position.x as f32 + size.x / 2.0,
|
||||
position.y as f32 + size.y / 2.0,
|
||||
);
|
||||
let left_top_tf = transform.position_from_value(&left_top);
|
||||
let right_bottom_tf = transform.position_from_value(&right_bottom);
|
||||
Rect::from_two_pos(left_top_tf, right_bottom_tf)
|
||||
};
|
||||
Image::new(*texture_id, *size)
|
||||
.bg_fill(*bg_fill)
|
||||
.tint(*tint)
|
||||
.uv(*uv)
|
||||
.paint_at(ui, rect);
|
||||
if *highlight {
|
||||
shapes.push(Shape::rect_stroke(
|
||||
rect,
|
||||
0.0,
|
||||
Stroke::new(1.0, ui.visuals().strong_text_color()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
Color32::TRANSPARENT
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
let mut bounds = Bounds::NOTHING;
|
||||
let left_top = Value::new(
|
||||
self.position.x as f32 - self.size.x / 2.0,
|
||||
self.position.y as f32 - self.size.y / 2.0,
|
||||
);
|
||||
let right_bottom = Value::new(
|
||||
self.position.x as f32 + self.size.x / 2.0,
|
||||
self.position.y as f32 + self.size.y / 2.0,
|
||||
);
|
||||
bounds.extend_with(&left_top);
|
||||
bounds.extend_with(&right_bottom);
|
||||
bounds
|
||||
}
|
||||
}
|
17
amdguid/src/items/plot_item.rs
Normal file
17
amdguid/src/items/plot_item.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use crate::items::Values;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::Ui;
|
||||
use epaint::{Color32, Shape};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Trait shared by things that can be drawn in the plot.
|
||||
pub trait PlotItem {
|
||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
||||
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
||||
fn name(&self) -> &str;
|
||||
fn color(&self) -> Color32;
|
||||
fn highlight(&mut self);
|
||||
fn highlighted(&self) -> bool;
|
||||
fn values(&self) -> Option<&Values>;
|
||||
fn get_bounds(&self) -> Bounds;
|
||||
}
|
239
amdguid/src/items/points.rs
Normal file
239
amdguid/src/items/points.rs
Normal file
@ -0,0 +1,239 @@
|
||||
use crate::items::marker_shape::MarkerShape;
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::{pos2, vec2, Pos2, Ui};
|
||||
use epaint::{Color32, Shape, Stroke};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// A set of points.
|
||||
pub struct Points {
|
||||
pub(crate) series: Values,
|
||||
pub(crate) shape: MarkerShape,
|
||||
/// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically.
|
||||
pub(crate) color: Color32,
|
||||
/// Whether to fill the marker. Does not apply to all types.
|
||||
pub(crate) filled: bool,
|
||||
/// The maximum extent of the marker from its center.
|
||||
pub(crate) radius: f32,
|
||||
pub(crate) name: String,
|
||||
pub(crate) highlight: bool,
|
||||
pub(crate) stems: Option<f32>,
|
||||
}
|
||||
|
||||
impl Points {
|
||||
pub fn new(series: Values) -> Self {
|
||||
Self {
|
||||
series,
|
||||
shape: MarkerShape::Circle,
|
||||
color: Color32::TRANSPARENT,
|
||||
filled: true,
|
||||
radius: 1.0,
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
stems: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the shape of the markers.
|
||||
pub fn shape(mut self, shape: MarkerShape) -> Self {
|
||||
self.shape = shape;
|
||||
self
|
||||
}
|
||||
|
||||
/// Highlight these points in the plot by scaling up their markers.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the marker's color.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to fill the marker.
|
||||
pub fn filled(mut self, filled: bool) -> Self {
|
||||
self.filled = filled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether to add stems between the markers and a horizontal reference line.
|
||||
pub fn stems(mut self, y_reference: impl Into<f32>) -> Self {
|
||||
self.stems = Some(y_reference.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum extent of the marker around its position.
|
||||
pub fn radius(mut self, radius: impl Into<f32>) -> Self {
|
||||
self.radius = radius.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this set of points.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for Points {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let sqrt_3 = 3f32.sqrt();
|
||||
let frac_sqrt_3_2 = 3f32.sqrt() / 2.0;
|
||||
let frac_1_sqrt_2 = 1.0 / 2f32.sqrt();
|
||||
|
||||
let Self {
|
||||
series,
|
||||
shape,
|
||||
color,
|
||||
filled,
|
||||
mut radius,
|
||||
highlight,
|
||||
stems,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let stroke_size = radius / 5.0;
|
||||
|
||||
let default_stroke = Stroke::new(stroke_size, *color);
|
||||
let mut stem_stroke = default_stroke;
|
||||
let stroke = (!filled)
|
||||
.then(|| default_stroke)
|
||||
.unwrap_or_else(Stroke::none);
|
||||
let fill = filled.then(|| *color).unwrap_or_default();
|
||||
|
||||
if *highlight {
|
||||
radius *= 2f32.sqrt();
|
||||
stem_stroke.width *= 2.0;
|
||||
}
|
||||
|
||||
let y_reference =
|
||||
stems.map(|y| transform.position_from_value(&Value::new(0.0, y)).y as f32);
|
||||
|
||||
series
|
||||
.values
|
||||
.iter()
|
||||
.map(|value| transform.position_from_value(value))
|
||||
.for_each(|center| {
|
||||
let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) };
|
||||
|
||||
if let Some(y) = y_reference {
|
||||
let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke);
|
||||
shapes.push(stem);
|
||||
}
|
||||
|
||||
match shape {
|
||||
MarkerShape::Circle => {
|
||||
shapes.push(Shape::Circle(epaint::CircleShape {
|
||||
center,
|
||||
radius,
|
||||
fill,
|
||||
stroke,
|
||||
}));
|
||||
}
|
||||
MarkerShape::Diamond => {
|
||||
let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Square => {
|
||||
let points = vec![
|
||||
tf(frac_1_sqrt_2, frac_1_sqrt_2),
|
||||
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
|
||||
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
|
||||
tf(-frac_1_sqrt_2, frac_1_sqrt_2),
|
||||
];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Cross => {
|
||||
let diagonal1 = [
|
||||
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
|
||||
tf(frac_1_sqrt_2, frac_1_sqrt_2),
|
||||
];
|
||||
let diagonal2 = [
|
||||
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
|
||||
tf(-frac_1_sqrt_2, frac_1_sqrt_2),
|
||||
];
|
||||
shapes.push(Shape::line_segment(diagonal1, default_stroke));
|
||||
shapes.push(Shape::line_segment(diagonal2, default_stroke));
|
||||
}
|
||||
MarkerShape::Plus => {
|
||||
let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)];
|
||||
let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
|
||||
shapes.push(Shape::line_segment(horizontal, default_stroke));
|
||||
shapes.push(Shape::line_segment(vertical, default_stroke));
|
||||
}
|
||||
MarkerShape::Up => {
|
||||
let points =
|
||||
vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Down => {
|
||||
let points = vec![
|
||||
tf(0.0, 1.0),
|
||||
tf(-0.5 * sqrt_3, -0.5),
|
||||
tf(0.5 * sqrt_3, -0.5),
|
||||
];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Left => {
|
||||
let points =
|
||||
vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Right => {
|
||||
let points = vec![
|
||||
tf(1.0, 0.0),
|
||||
tf(-0.5, -0.5 * sqrt_3),
|
||||
tf(-0.5, 0.5 * sqrt_3),
|
||||
];
|
||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
||||
}
|
||||
MarkerShape::Asterisk => {
|
||||
let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
|
||||
let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)];
|
||||
let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)];
|
||||
shapes.push(Shape::line_segment(vertical, default_stroke));
|
||||
shapes.push(Shape::line_segment(diagonal1, default_stroke));
|
||||
shapes.push(Shape::line_segment(diagonal2, default_stroke));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
||||
self.series.generate_points(x_range);
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
Some(&self.series)
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
self.series.get_bounds()
|
||||
}
|
||||
}
|
137
amdguid/src/items/polygons.rs
Normal file
137
amdguid/src/items/polygons.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::values::Values;
|
||||
use crate::items::{LineStyle, DEFAULT_FILL_ALPHA};
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::{NumExt, Ui};
|
||||
use epaint::{Color32, Rgba, Shape, Stroke};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// A convex polygon.
|
||||
pub struct Polygon {
|
||||
pub series: Values,
|
||||
pub stroke: Stroke,
|
||||
pub name: String,
|
||||
pub highlight: bool,
|
||||
pub fill_alpha: f32,
|
||||
pub style: LineStyle,
|
||||
}
|
||||
|
||||
impl Polygon {
|
||||
pub fn new(series: Values) -> Self {
|
||||
Self {
|
||||
series,
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
fill_alpha: DEFAULT_FILL_ALPHA,
|
||||
style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this polygon in the plot by scaling up the stroke and reducing the fill
|
||||
/// transparency.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom stroke.
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the stroke width.
|
||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
||||
self.stroke.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.stroke.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Alpha of the filled area.
|
||||
pub fn fill_alpha(mut self, alpha: impl Into<f32>) -> Self {
|
||||
self.fill_alpha = alpha.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the outline's style. Default is `LineStyle::Solid`.
|
||||
pub fn style(mut self, style: LineStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this polygon.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for Polygon {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let Self {
|
||||
series,
|
||||
stroke,
|
||||
highlight,
|
||||
mut fill_alpha,
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
|
||||
if *highlight {
|
||||
fill_alpha = (2.0 * fill_alpha).at_most(1.0);
|
||||
}
|
||||
|
||||
let mut values_tf: Vec<_> = series
|
||||
.values
|
||||
.iter()
|
||||
.map(|v| transform.position_from_value(v))
|
||||
.collect();
|
||||
|
||||
let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha);
|
||||
|
||||
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none());
|
||||
shapes.push(shape);
|
||||
values_tf.push(*values_tf.first().unwrap());
|
||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
||||
self.series.generate_points(x_range);
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.stroke.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
Some(&self.series)
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
self.series.get_bounds()
|
||||
}
|
||||
}
|
122
amdguid/src/items/text.rs
Normal file
122
amdguid/src/items/text.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::{Align2, Rect, Ui};
|
||||
use epaint::{Color32, Shape, Stroke, TextStyle};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
/// Text inside the plot.
|
||||
pub struct Text {
|
||||
pub(crate) text: String,
|
||||
pub(crate) style: TextStyle,
|
||||
pub(crate) position: Value,
|
||||
pub(crate) name: String,
|
||||
pub(crate) highlight: bool,
|
||||
pub(crate) color: Color32,
|
||||
pub(crate) anchor: Align2,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn new(position: Value, text: impl ToString) -> Self {
|
||||
Self {
|
||||
text: text.to_string(),
|
||||
style: TextStyle::Small,
|
||||
position,
|
||||
name: Default::default(),
|
||||
highlight: false,
|
||||
color: Color32::TRANSPARENT,
|
||||
anchor: Align2::CENTER_CENTER,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this text in the plot by drawing a rectangle around it.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Text style. Default is `TextStyle::Small`.
|
||||
pub fn style(mut self, style: TextStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Text color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Anchor position of the text. Default is `Align2::CENTER_CENTER`.
|
||||
pub fn anchor(mut self, anchor: Align2) -> Self {
|
||||
self.anchor = anchor;
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this text.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotItem for Text {
|
||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let color = if self.color == Color32::TRANSPARENT {
|
||||
ui.style().visuals.text_color()
|
||||
} else {
|
||||
self.color
|
||||
};
|
||||
let pos = transform.position_from_value(&self.position);
|
||||
let galley = ui
|
||||
.fonts()
|
||||
.layout_no_wrap(self.text.clone(), self.style, color);
|
||||
let rect = self
|
||||
.anchor
|
||||
.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
||||
shapes.push(Shape::galley(rect.min, galley));
|
||||
if self.highlight {
|
||||
shapes.push(Shape::rect_stroke(
|
||||
rect.expand(2.0),
|
||||
1.0,
|
||||
Stroke::new(0.5, color),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
let mut bounds = Bounds::NOTHING;
|
||||
bounds.extend_with(&self.position);
|
||||
bounds
|
||||
}
|
||||
}
|
118
amdguid/src/items/v_line.rs
Normal file
118
amdguid/src/items/v_line.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use crate::items::plot_item::PlotItem;
|
||||
use crate::items::value::Value;
|
||||
use crate::items::values::Values;
|
||||
use crate::items::LineStyle;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use egui::Ui;
|
||||
use epaint::{Color32, Shape, Stroke};
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
impl PlotItem for VLine {
|
||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
||||
let VLine {
|
||||
x,
|
||||
stroke,
|
||||
highlight,
|
||||
style,
|
||||
..
|
||||
} = self;
|
||||
let points = vec![
|
||||
transform.position_from_value(&Value::new(*x, transform.bounds().min[1])),
|
||||
transform.position_from_value(&Value::new(*x, transform.bounds().max[1])),
|
||||
];
|
||||
style.style_line(points, *stroke, *highlight, shapes);
|
||||
}
|
||||
|
||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn color(&self) -> Color32 {
|
||||
self.stroke.color
|
||||
}
|
||||
|
||||
fn highlight(&mut self) {
|
||||
self.highlight = true;
|
||||
}
|
||||
|
||||
fn highlighted(&self) -> bool {
|
||||
self.highlight
|
||||
}
|
||||
|
||||
fn values(&self) -> Option<&Values> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_bounds(&self) -> Bounds {
|
||||
let mut bounds = Bounds::NOTHING;
|
||||
bounds.min[0] = self.x;
|
||||
bounds.max[0] = self.x;
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertical line in a plot, filling the full width
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VLine {
|
||||
pub(crate) x: f64,
|
||||
pub(crate) stroke: Stroke,
|
||||
pub(crate) name: String,
|
||||
pub(crate) highlight: bool,
|
||||
pub(crate) style: LineStyle,
|
||||
}
|
||||
|
||||
impl VLine {
|
||||
pub fn new(x: impl Into<f64>) -> Self {
|
||||
Self {
|
||||
x: x.into(),
|
||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
||||
name: String::default(),
|
||||
highlight: false,
|
||||
style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Highlight this line in the plot by scaling up the line.
|
||||
pub fn highlight(mut self) -> Self {
|
||||
self.highlight = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a stroke.
|
||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
||||
self.stroke = stroke.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke width. A high value means the plot thickens.
|
||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
||||
self.stroke.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
|
||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
||||
self.stroke.color = color.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the line's style. Default is `LineStyle::Solid`.
|
||||
pub fn style(mut self, style: LineStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Name of this vertical line.
|
||||
///
|
||||
/// This name will show up in the plot legend, if legends are turned on.
|
||||
///
|
||||
/// Multiple plot items may share the same name, in which case they will also share an entry in
|
||||
/// the legend.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
22
amdguid/src/items/value.rs
Normal file
22
amdguid/src/items/value.rs
Normal file
@ -0,0 +1,22 @@
|
||||
/// A value in the value-space of the plot.
|
||||
///
|
||||
/// Uses f64 for improved accuracy to enable plotting
|
||||
/// large values (e.g. unix time on x axis).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Value {
|
||||
/// This is often something monotonically increasing, such as time, but doesn't have to be.
|
||||
/// Goes from left to right.
|
||||
pub x: f64,
|
||||
/// Goes from bottom to top (inverse of everything else in egui!).
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
#[inline(always)]
|
||||
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
|
||||
Self {
|
||||
x: x.into(),
|
||||
y: y.into(),
|
||||
}
|
||||
}
|
||||
}
|
135
amdguid/src/items/values.rs
Normal file
135
amdguid/src/items/values.rs
Normal file
@ -0,0 +1,135 @@
|
||||
use crate::items::{ExplicitGenerator, Value};
|
||||
use crate::transform::Bounds;
|
||||
use std::collections::Bound;
|
||||
use std::ops::{RangeBounds, RangeInclusive};
|
||||
|
||||
pub struct Values {
|
||||
pub values: Vec<Value>,
|
||||
generator: Option<ExplicitGenerator>,
|
||||
}
|
||||
|
||||
impl Values {
|
||||
pub fn from_values(values: Vec<Value>) -> Self {
|
||||
Self {
|
||||
values,
|
||||
generator: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_values_iter(iter: impl Iterator<Item = Value>) -> Self {
|
||||
Self::from_values(iter.collect())
|
||||
}
|
||||
|
||||
/// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points.
|
||||
pub fn from_explicit_callback(
|
||||
function: impl Fn(f64) -> f64 + 'static,
|
||||
x_range: impl RangeBounds<f64>,
|
||||
points: usize,
|
||||
) -> Self {
|
||||
let start = match x_range.start_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||
Bound::Unbounded => f64::NEG_INFINITY,
|
||||
};
|
||||
let end = match x_range.end_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
||||
Bound::Unbounded => f64::INFINITY,
|
||||
};
|
||||
let x_range = start..=end;
|
||||
|
||||
let generator = ExplicitGenerator {
|
||||
function: Box::new(function),
|
||||
x_range,
|
||||
points,
|
||||
};
|
||||
|
||||
Self {
|
||||
values: Vec::new(),
|
||||
generator: Some(generator),
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points.
|
||||
/// The range may be specified as start..end or as start..=end.
|
||||
pub fn from_parametric_callback(
|
||||
function: impl Fn(f64) -> (f64, f64),
|
||||
t_range: impl RangeBounds<f64>,
|
||||
points: usize,
|
||||
) -> Self {
|
||||
let start = match t_range.start_bound() {
|
||||
Bound::Included(x) => x,
|
||||
Bound::Excluded(_) => unreachable!(),
|
||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||
};
|
||||
let end = match t_range.end_bound() {
|
||||
Bound::Included(x) | Bound::Excluded(x) => x,
|
||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
||||
};
|
||||
let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
|
||||
let increment = if last_point_included {
|
||||
(end - start) / (points - 1) as f64
|
||||
} else {
|
||||
(end - start) / points as f64
|
||||
};
|
||||
let values = (0..points).map(|i| {
|
||||
let t = start + i as f64 * increment;
|
||||
let (x, y) = function(t);
|
||||
Value { x, y }
|
||||
});
|
||||
Self::from_values_iter(values)
|
||||
}
|
||||
|
||||
/// From a series of y-values.
|
||||
/// The x-values will be the indices of these values
|
||||
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
||||
let values: Vec<Value> = ys
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &y)| Value {
|
||||
x: i as f64,
|
||||
y: y as f64,
|
||||
})
|
||||
.collect();
|
||||
Self::from_values(values)
|
||||
}
|
||||
|
||||
/// Returns true if there are no data points available and there is no function to generate any.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.generator.is_none() && self.values.is_empty()
|
||||
}
|
||||
|
||||
/// If initialized with a generator function, this will generate `n` evenly spaced points in the
|
||||
/// given range.
|
||||
pub fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
|
||||
if let Some(generator) = self.generator.take() {
|
||||
if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) {
|
||||
let increment =
|
||||
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
||||
self.values = (0..generator.points)
|
||||
.map(|i| {
|
||||
let x = intersection.start() + i as f64 * increment;
|
||||
let y = (generator.function)(x);
|
||||
Value { x, y }
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the intersection of two ranges if they intersect.
|
||||
fn range_intersection(
|
||||
range1: &RangeInclusive<f64>,
|
||||
range2: &RangeInclusive<f64>,
|
||||
) -> Option<RangeInclusive<f64>> {
|
||||
let start = range1.start().max(*range2.start());
|
||||
let end = range1.end().min(*range2.end());
|
||||
(start < end).then(|| start..=end)
|
||||
}
|
||||
|
||||
pub(crate) fn get_bounds(&self) -> Bounds {
|
||||
let mut bounds = Bounds::NOTHING;
|
||||
self.values
|
||||
.iter()
|
||||
.for_each(|value| bounds.extend_with(value));
|
||||
bounds
|
||||
}
|
||||
}
|
37
amdguid/src/main.rs
Normal file
37
amdguid/src/main.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use app::AmdGui;
|
||||
|
||||
mod app;
|
||||
mod backend;
|
||||
pub mod items;
|
||||
pub mod transform;
|
||||
pub mod widgets;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "DEBUG");
|
||||
}
|
||||
pretty_env_logger::init();
|
||||
let config = Arc::new(Mutex::new(
|
||||
amdgpu_config::fan::load_config(amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH)
|
||||
.expect("No FAN config"),
|
||||
));
|
||||
let amd_gui = Arc::new(Mutex::new(AmdGui::new_with_config(config)));
|
||||
|
||||
schedule_tick(amd_gui.clone());
|
||||
|
||||
backend::run_app(amd_gui);
|
||||
}
|
||||
|
||||
fn schedule_tick(amd_gui: std::sync::Arc<parking_lot::Mutex<AmdGui>>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
amd_gui.lock().tick();
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(166)).await;
|
||||
}
|
||||
});
|
||||
}
|
243
amdguid/src/transform.rs
Normal file
243
amdguid/src/transform.rs
Normal file
@ -0,0 +1,243 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use egui::{pos2, remap, Pos2, Rect, Vec2};
|
||||
|
||||
use crate::items::Value;
|
||||
|
||||
/// 2D bounding box of f64 precision.
|
||||
/// The range of data values we show.
|
||||
#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Bounds {
|
||||
pub min: [f64; 2],
|
||||
pub max: [f64; 2],
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
pub const NOTHING: Self = Self {
|
||||
min: [f64::INFINITY; 2],
|
||||
max: [-f64::INFINITY; 2],
|
||||
};
|
||||
|
||||
pub fn new_symmetrical(half_extent: f64) -> Self {
|
||||
Self {
|
||||
min: [-half_extent; 2],
|
||||
max: [half_extent; 2],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_finite(&self) -> bool {
|
||||
self.min[0].is_finite()
|
||||
&& self.min[1].is_finite()
|
||||
&& self.max[0].is_finite()
|
||||
&& self.max[1].is_finite()
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 {
|
||||
self.max[0] - self.min[0]
|
||||
}
|
||||
|
||||
pub fn height(&self) -> f64 {
|
||||
self.max[1] - self.min[1]
|
||||
}
|
||||
|
||||
pub fn extend_with(&mut self, value: &Value) {
|
||||
self.extend_with_x(value.x);
|
||||
self.extend_with_y(value.y);
|
||||
}
|
||||
|
||||
/// Expand to include the given x coordinate
|
||||
pub fn extend_with_x(&mut self, x: f64) {
|
||||
self.min[0] = self.min[0].min(x);
|
||||
self.max[0] = self.max[0].max(x);
|
||||
}
|
||||
|
||||
/// Expand to include the given y coordinate
|
||||
pub fn extend_with_y(&mut self, y: f64) {
|
||||
self.min[1] = self.min[1].min(y);
|
||||
self.max[1] = self.max[1].max(y);
|
||||
}
|
||||
|
||||
pub fn expand_x(&mut self, pad: f64) {
|
||||
self.min[0] -= pad;
|
||||
self.max[0] += pad;
|
||||
}
|
||||
|
||||
pub fn expand_y(&mut self, pad: f64) {
|
||||
self.min[1] -= pad;
|
||||
self.max[1] += pad;
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &Bounds) {
|
||||
self.min[0] = self.min[0].min(other.min[0]);
|
||||
self.min[1] = self.min[1].min(other.min[1]);
|
||||
self.max[0] = self.max[0].max(other.max[0]);
|
||||
self.max[1] = self.max[1].max(other.max[1]);
|
||||
}
|
||||
|
||||
pub fn translate_x(&mut self, delta: f64) {
|
||||
self.min[0] += delta;
|
||||
self.max[0] += delta;
|
||||
}
|
||||
|
||||
pub fn translate_y(&mut self, delta: f64) {
|
||||
self.min[1] += delta;
|
||||
self.max[1] += delta;
|
||||
}
|
||||
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
self.translate_x(delta.x as f64);
|
||||
self.translate_y(delta.y as f64);
|
||||
}
|
||||
|
||||
pub fn add_relative_margin(&mut self, margin_fraction: Vec2) {
|
||||
let width = self.width().max(0.0);
|
||||
let height = self.height().max(0.0);
|
||||
self.expand_x(margin_fraction.x as f64 * width);
|
||||
self.expand_y(margin_fraction.y as f64 * height);
|
||||
}
|
||||
|
||||
pub fn range_x(&self) -> RangeInclusive<f64> {
|
||||
self.min[0]..=self.max[0]
|
||||
}
|
||||
|
||||
pub fn make_x_symmetrical(&mut self) {
|
||||
let x_abs = self.min[0].abs().max(self.max[0].abs());
|
||||
self.min[0] = -x_abs;
|
||||
self.max[0] = x_abs;
|
||||
}
|
||||
|
||||
pub fn make_y_symmetrical(&mut self) {
|
||||
let y_abs = self.min[1].abs().max(self.max[1].abs());
|
||||
self.min[1] = -y_abs;
|
||||
self.max[1] = y_abs;
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the screen rectangle and the plot bounds and provides methods to transform them.
|
||||
#[derive(Clone)]
|
||||
pub struct ScreenTransform {
|
||||
/// The screen rectangle.
|
||||
frame: Rect,
|
||||
/// The plot bounds.
|
||||
bounds: Bounds,
|
||||
/// Whether to always center the x-range of the bounds.
|
||||
x_centered: bool,
|
||||
/// Whether to always center the y-range of the bounds.
|
||||
y_centered: bool,
|
||||
}
|
||||
|
||||
impl ScreenTransform {
|
||||
pub fn new(frame: Rect, bounds: Bounds, x_centered: bool, y_centered: bool) -> Self {
|
||||
Self {
|
||||
frame,
|
||||
bounds,
|
||||
x_centered,
|
||||
y_centered,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> &Rect {
|
||||
&self.frame
|
||||
}
|
||||
|
||||
pub fn bounds(&self) -> &Bounds {
|
||||
&self.bounds
|
||||
}
|
||||
|
||||
pub fn translate_bounds(&mut self, mut delta_pos: Vec2) {
|
||||
if self.x_centered {
|
||||
delta_pos.x = 0.;
|
||||
}
|
||||
if self.y_centered {
|
||||
delta_pos.y = 0.;
|
||||
}
|
||||
delta_pos.x *= self.dvalue_dpos()[0] as f32;
|
||||
delta_pos.y *= self.dvalue_dpos()[1] as f32;
|
||||
self.bounds.translate(delta_pos);
|
||||
}
|
||||
|
||||
/// Zoom by a relative factor with the given screen position as center.
|
||||
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
|
||||
let center = self.value_from_position(center);
|
||||
|
||||
let mut new_bounds = self.bounds;
|
||||
new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64);
|
||||
new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64);
|
||||
new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64);
|
||||
new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64);
|
||||
|
||||
if new_bounds.is_valid() {
|
||||
self.bounds = new_bounds;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_from_value(&self, value: &Value) -> Pos2 {
|
||||
let x = remap(
|
||||
value.x,
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||
);
|
||||
let y = remap(
|
||||
value.y,
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||
);
|
||||
pos2(x as f32, y as f32)
|
||||
}
|
||||
|
||||
pub fn value_from_position(&self, pos: Pos2) -> Value {
|
||||
let x = remap(
|
||||
pos.x as f64,
|
||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
||||
self.bounds.min[0]..=self.bounds.max[0],
|
||||
);
|
||||
let y = remap(
|
||||
pos.y as f64,
|
||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
||||
self.bounds.min[1]..=self.bounds.max[1],
|
||||
);
|
||||
Value::new(x, y)
|
||||
}
|
||||
|
||||
/// delta position / delta value
|
||||
pub fn dpos_dvalue_x(&self) -> f64 {
|
||||
self.frame.width() as f64 / self.bounds.width()
|
||||
}
|
||||
|
||||
/// delta position / delta value
|
||||
pub fn dpos_dvalue_y(&self) -> f64 {
|
||||
-self.frame.height() as f64 / self.bounds.height() // negated y axis!
|
||||
}
|
||||
|
||||
/// delta position / delta value
|
||||
pub fn dpos_dvalue(&self) -> [f64; 2] {
|
||||
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
|
||||
}
|
||||
|
||||
/// delta value / delta position
|
||||
pub fn dvalue_dpos(&self) -> [f64; 2] {
|
||||
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
|
||||
}
|
||||
|
||||
pub fn get_aspect(&self) -> f64 {
|
||||
let rw = self.frame.width() as f64;
|
||||
let rh = self.frame.height() as f64;
|
||||
(self.bounds.width() / rw) / (self.bounds.height() / rh)
|
||||
}
|
||||
|
||||
pub fn set_aspect(&mut self, aspect: f64) {
|
||||
let epsilon = 1e-5;
|
||||
let current_aspect = self.get_aspect();
|
||||
if current_aspect < aspect - epsilon {
|
||||
self.bounds
|
||||
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
|
||||
} else if current_aspect > aspect + epsilon {
|
||||
self.bounds
|
||||
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
165
amdguid/src/widgets/change_fan_settings.rs
Normal file
165
amdguid/src/widgets/change_fan_settings.rs
Normal file
@ -0,0 +1,165 @@
|
||||
use amdgpu::helper_cmd::Command;
|
||||
use amdgpu_config::fan::MatrixPoint;
|
||||
use egui::{emath, pos2, Layout, PointerButton, Ui};
|
||||
use epaint::Color32;
|
||||
|
||||
use crate::app::{ChangeState, FanConfig, FanServices, StatefulConfig};
|
||||
use crate::widgets::drag_plot::PlotMsg;
|
||||
use crate::widgets::reload_section::ReloadSection;
|
||||
use crate::{widgets, widgets::ConfigFile};
|
||||
|
||||
pub struct ChangeFanSettings {
|
||||
config: FanConfig,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl ChangeFanSettings {
|
||||
pub fn new(config: FanConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(&mut self, idx: usize) {
|
||||
self.selected = Some(idx);
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.selected = None;
|
||||
}
|
||||
|
||||
pub fn draw(&mut self, ui: &mut Ui, pid_files: &mut FanServices, state: &mut StatefulConfig) {
|
||||
let available = ui.available_rect_before_wrap();
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.child_ui(
|
||||
emath::Rect {
|
||||
min: available.min,
|
||||
max: pos2(available.width() / 2.0, available.height()),
|
||||
},
|
||||
Layout::left_to_right(),
|
||||
)
|
||||
.vertical(|ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.enable_scrolling(true)
|
||||
.id_source("plot-and-reload")
|
||||
.show(ui, |ui| {
|
||||
ui.add({
|
||||
let curve = {
|
||||
let config = self.config.lock();
|
||||
let iter = config
|
||||
.speed_matrix()
|
||||
.iter()
|
||||
.map(|v| crate::items::Value::new(v.speed, v.temp));
|
||||
crate::items::Line::new(crate::items::Values::from_values_iter(
|
||||
iter,
|
||||
))
|
||||
.color(Color32::BLUE)
|
||||
};
|
||||
widgets::drag_plot::DragPlot::new("change fan settings")
|
||||
.height(600.0)
|
||||
.width(available.width() / 2.0)
|
||||
.selected(self.selected)
|
||||
.allow_drag(true)
|
||||
.allow_zoom(false)
|
||||
.line(curve)
|
||||
.y_axis_name(String::from("Temperature"))
|
||||
.x_axis_name(String::from("Speed"))
|
||||
.hline(crate::items::HLine::new(100.0).color(Color32::TRANSPARENT))
|
||||
.vline(crate::items::VLine::new(100.0).color(Color32::TRANSPARENT))
|
||||
.on_event(|msg| match msg {
|
||||
PlotMsg::Clicked(idx) => {
|
||||
self.selected = Some(idx);
|
||||
}
|
||||
PlotMsg::Drag(delta) => {
|
||||
if let Some(idx) = self.selected {
|
||||
let mut config = self.config.lock();
|
||||
let min = idx
|
||||
.checked_sub(1)
|
||||
.and_then(|i| config.speed_matrix().get(i).copied())
|
||||
.unwrap_or(MatrixPoint::MIN);
|
||||
let max = idx
|
||||
.checked_add(1)
|
||||
.and_then(|i| config.speed_matrix().get(i).copied())
|
||||
.unwrap_or(MatrixPoint::MAX);
|
||||
let current = config.speed_matrix_mut().get_mut(idx);
|
||||
|
||||
if let Some(point) = current {
|
||||
point.speed = (point.speed + delta.x as f64)
|
||||
.max(min.speed)
|
||||
.min(max.speed);
|
||||
point.temp = (point.temp + delta.y as f64)
|
||||
.max(min.temp)
|
||||
.min(max.temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.legend(crate::widgets::legend::Legend::default())
|
||||
});
|
||||
ui.separator();
|
||||
Self::save_button(self.config.clone(), state, ui);
|
||||
ui.add(ReloadSection::new(pid_files));
|
||||
});
|
||||
});
|
||||
|
||||
ui.child_ui(
|
||||
emath::Rect {
|
||||
min: pos2(available.width() / 2.0 + 20.0, available.min.y),
|
||||
max: available.max,
|
||||
},
|
||||
Layout::left_to_right(),
|
||||
)
|
||||
.vertical(|ui| {
|
||||
ui.add(ConfigFile::new(self.config.clone()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn save_button(config: FanConfig, state: &mut StatefulConfig, ui: &mut Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save").clicked_by(PointerButton::Primary) {
|
||||
Self::save_config(config, state);
|
||||
}
|
||||
match &state.state {
|
||||
ChangeState::New => {}
|
||||
ChangeState::Reloading => {
|
||||
ui.label("Saving...");
|
||||
}
|
||||
ChangeState::Success => {
|
||||
ui.add(egui::Label::new("Saved").text_color(Color32::GREEN));
|
||||
}
|
||||
ChangeState::Failure(msg) => {
|
||||
ui.add(egui::Label::new(format!("Failure. {}", msg)).text_color(Color32::RED));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn save_config(config: FanConfig, state: &mut StatefulConfig) {
|
||||
state.state = ChangeState::Reloading;
|
||||
|
||||
let config = config.lock();
|
||||
let c: &amdgpu_config::fan::Config = &*config;
|
||||
let content = match toml::to_string(c) {
|
||||
Err(e) => {
|
||||
log::error!("Config file serialization failed. {:?}", e);
|
||||
return;
|
||||
}
|
||||
Ok(content) => content,
|
||||
};
|
||||
let command = Command::SaveFanConfig {
|
||||
path: String::from(config.path()),
|
||||
content,
|
||||
};
|
||||
match amdgpu::helper_cmd::send_command(command) {
|
||||
Ok(amdgpu::helper_cmd::Response::ConfigFileSaveFailed(msg)) => {
|
||||
state.state = ChangeState::Failure(msg);
|
||||
}
|
||||
Ok(amdgpu::helper_cmd::Response::ConfigFileSaved) => {
|
||||
state.state = ChangeState::Success;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
112
amdguid/src/widgets/config_file.rs
Normal file
112
amdguid/src/widgets/config_file.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use egui::{PointerButton, Response, Sense, Ui, Widget};
|
||||
|
||||
use amdgpu_config::fan::MatrixPoint;
|
||||
|
||||
use crate::app::FanConfig;
|
||||
|
||||
pub struct ConfigFile {
|
||||
config: FanConfig,
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
pub fn new(config: FanConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ConfigFile {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
ui.vertical(|ui| {
|
||||
let mut matrix = self
|
||||
.config
|
||||
.lock()
|
||||
.speed_matrix()
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.peekable();
|
||||
|
||||
let mut prev = None;
|
||||
|
||||
while let Some((idx, mut current)) = matrix.next() {
|
||||
let min = if current == MatrixPoint::MIN {
|
||||
MatrixPoint::MIN
|
||||
} else if let Some(prev) = prev {
|
||||
prev
|
||||
} else {
|
||||
MatrixPoint::MIN
|
||||
};
|
||||
let next = matrix.peek();
|
||||
let max = if current == MatrixPoint::MAX {
|
||||
MatrixPoint::MAX
|
||||
} else if let Some(next) = next.map(|(_, n)| n) {
|
||||
*next
|
||||
} else {
|
||||
MatrixPoint::MAX
|
||||
};
|
||||
|
||||
{
|
||||
ui.label("Temperature");
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut current.temp, min.temp..=max.temp))
|
||||
.changed()
|
||||
{
|
||||
if let Some(entry) = self.config.lock().speed_matrix_mut().get_mut(idx) {
|
||||
entry.temp = current.temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
ui.label("Speed");
|
||||
if ui
|
||||
.add(egui::Slider::new(&mut current.speed, min.speed..=max.speed))
|
||||
.changed()
|
||||
{
|
||||
if let Some(entry) = self.config.lock().speed_matrix_mut().get_mut(idx) {
|
||||
entry.speed = current.speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if next.is_some() {
|
||||
if ui
|
||||
.add(egui::Button::new("Add in middle"))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
self.config.lock().speed_matrix_vec_mut().insert(
|
||||
idx + 1,
|
||||
MatrixPoint::new(
|
||||
min.temp + ((max.temp - min.temp) / 2.0),
|
||||
min.speed + ((max.speed - min.speed) / 2.0),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if next.is_none()
|
||||
&& current != MatrixPoint::MAX
|
||||
&& ui
|
||||
.add(egui::Button::new("Add"))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
self.config
|
||||
.lock()
|
||||
.speed_matrix_vec_mut()
|
||||
.push(MatrixPoint::new(100.0, 100.0))
|
||||
}
|
||||
if ui
|
||||
.add(egui::Button::new("Remove"))
|
||||
.clicked_by(PointerButton::Primary)
|
||||
{
|
||||
self.config.lock().speed_matrix_vec_mut().remove(idx);
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
prev = Some(current);
|
||||
}
|
||||
|
||||
ui.allocate_response(ui.available_size(), Sense::click())
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
90
amdguid/src/widgets/cooling_performance.rs
Normal file
90
amdguid/src/widgets/cooling_performance.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use crate::app::{FanConfig, FanServices};
|
||||
use amdgpu::Card;
|
||||
use amdmond_lib::AmdMon;
|
||||
use core::option::Option;
|
||||
use core::option::Option::Some;
|
||||
use egui::Ui;
|
||||
use std::collections::vec_deque::VecDeque;
|
||||
|
||||
pub struct CoolingPerformance {
|
||||
capacity: usize,
|
||||
data: VecDeque<f64>,
|
||||
amd_mon: Option<AmdMon>,
|
||||
}
|
||||
|
||||
impl CoolingPerformance {
|
||||
pub fn new(capacity: usize, fan_config: FanConfig) -> Self {
|
||||
let amd_mon = amdgpu::hw_mon::open_hw_mon(Card(0))
|
||||
.map(|hw| amdmond_lib::AmdMon::wrap(hw, &*fan_config.lock()))
|
||||
.ok();
|
||||
|
||||
Self {
|
||||
capacity,
|
||||
data: VecDeque::with_capacity(capacity),
|
||||
amd_mon,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
if let Some(temp) = self
|
||||
.amd_mon
|
||||
.as_ref()
|
||||
.and_then(|mon| mon.gpu_temp_of(0))
|
||||
.and_then(|(_, value)| value.ok())
|
||||
{
|
||||
self.push(temp as f64);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw(&self, ui: &mut Ui, pid_files: &FanServices) {
|
||||
use egui::widgets::plot::*;
|
||||
use epaint::color::Color32;
|
||||
|
||||
let current = self.data.iter().last().copied().unwrap_or_default();
|
||||
|
||||
let iter = self
|
||||
.data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, v)| Value::new(i as f64, *v));
|
||||
|
||||
let curve = Line::new(Values::from_values_iter(iter)).color(Color32::BLUE);
|
||||
let zero = HLine::new(0.0).color(Color32::from_white_alpha(0));
|
||||
let optimal = HLine::new(45.0).name("Optimal").color(Color32::LIGHT_BLUE);
|
||||
let target = HLine::new(80.0)
|
||||
.name("Overheating")
|
||||
.color(Color32::DARK_RED);
|
||||
|
||||
let plot = Plot::new("cooling performance")
|
||||
.allow_drag(false)
|
||||
.allow_zoom(false)
|
||||
.height(600.0)
|
||||
.line(curve)
|
||||
.hline(zero)
|
||||
.hline(optimal)
|
||||
.hline(target)
|
||||
.legend(Legend::default());
|
||||
|
||||
ui.label("Temperature");
|
||||
ui.add(plot);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Current temperature");
|
||||
ui.label(format!("{:<3.2}°C", current));
|
||||
});
|
||||
ui.label("Working services");
|
||||
if pid_files.0.is_empty() {
|
||||
ui.label(" There's no working services");
|
||||
} else {
|
||||
pid_files.0.iter().for_each(|service| {
|
||||
ui.label(format!(" {}", service.pid.0));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, v: f64) {
|
||||
if self.data.len() >= self.capacity {
|
||||
self.data.pop_front();
|
||||
}
|
||||
self.data.push_back(v);
|
||||
}
|
||||
}
|
415
amdguid/src/widgets/drag_plot.rs
Normal file
415
amdguid/src/widgets/drag_plot.rs
Normal file
@ -0,0 +1,415 @@
|
||||
use egui::{emath, vec2, CursorIcon, Id, NumExt, PointerButton, Response, Sense, Ui, Vec2};
|
||||
use epaint::ahash::AHashSet;
|
||||
use epaint::color::Hsva;
|
||||
use epaint::Color32;
|
||||
|
||||
use crate::items::HLine;
|
||||
use crate::items::*;
|
||||
use crate::transform::{Bounds, ScreenTransform};
|
||||
use crate::widgets::drag_plot_prepared::DragPlotPrepared;
|
||||
use crate::widgets::legend::Legend;
|
||||
use crate::widgets::legend_widget::LegendWidget;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PlotMsg {
|
||||
Clicked(usize),
|
||||
Drag(emath::Vec2),
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct PlotMemory {
|
||||
bounds: Bounds,
|
||||
auto_bounds: bool,
|
||||
hovered_entry: Option<String>,
|
||||
hidden_items: AHashSet<String>,
|
||||
min_auto_bounds: Bounds,
|
||||
}
|
||||
|
||||
pub struct DragPlot<OnEvent>
|
||||
where
|
||||
OnEvent: FnMut(PlotMsg),
|
||||
{
|
||||
id: egui::Id,
|
||||
|
||||
items: Vec<Box<dyn PlotItem>>,
|
||||
|
||||
min_auto_bounds: Bounds,
|
||||
min_size: Vec2,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
data_aspect: Option<f32>,
|
||||
view_aspect: Option<f32>,
|
||||
legend_config: Option<Legend>,
|
||||
|
||||
next_auto_color_idx: usize,
|
||||
allow_zoom: bool,
|
||||
allow_drag: bool,
|
||||
margin_fraction: Vec2,
|
||||
selected: Option<usize>,
|
||||
on_event: Option<OnEvent>,
|
||||
|
||||
lines: Vec<Line>,
|
||||
axis_names: [String; 2],
|
||||
}
|
||||
|
||||
impl<OnEvent> DragPlot<OnEvent>
|
||||
where
|
||||
OnEvent: FnMut(PlotMsg),
|
||||
{
|
||||
pub fn new(id_source: impl std::hash::Hash) -> Self {
|
||||
Self {
|
||||
id: Id::new(id_source),
|
||||
|
||||
items: Default::default(),
|
||||
min_size: Vec2::splat(64.0),
|
||||
width: None,
|
||||
height: None,
|
||||
data_aspect: None,
|
||||
view_aspect: None,
|
||||
min_auto_bounds: Bounds::NOTHING,
|
||||
legend_config: None,
|
||||
next_auto_color_idx: 0,
|
||||
allow_zoom: true,
|
||||
allow_drag: true,
|
||||
margin_fraction: Vec2::splat(0.05),
|
||||
selected: None,
|
||||
on_event: None,
|
||||
lines: vec![],
|
||||
axis_names: [String::from("x"), String::from("y")],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn x_axis_name(mut self, name: String) -> Self {
|
||||
self.axis_names[0] = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn y_axis_name(mut self, name: String) -> Self {
|
||||
self.axis_names[1] = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a legend including all named items.
|
||||
pub fn legend(mut self, legend: Legend) -> Self {
|
||||
self.legend_config = Some(legend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a data lines.
|
||||
pub fn line(mut self, mut line: Line) -> Self {
|
||||
if line.series.is_empty() {
|
||||
return self;
|
||||
};
|
||||
|
||||
// Give the stroke an automatic color if no color has been assigned.
|
||||
if line.stroke.color == Color32::TRANSPARENT {
|
||||
line.stroke.color = self.auto_color();
|
||||
}
|
||||
self.lines.push(line);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn auto_color(&mut self) -> Color32 {
|
||||
let i = self.next_auto_color_idx;
|
||||
self.next_auto_color_idx += 1;
|
||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
||||
let h = i as f32 * golden_ratio;
|
||||
Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other perspective color space
|
||||
}
|
||||
|
||||
/// width / height ratio of the data.
|
||||
/// For instance, it can be useful to set this to `1.0` for when the two axes show the same
|
||||
/// unit.
|
||||
/// By default the plot window's aspect ratio is used.
|
||||
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
|
||||
self.data_aspect = Some(data_aspect);
|
||||
self
|
||||
}
|
||||
|
||||
/// width / height ratio of the plot region.
|
||||
/// By default no fixed aspect ratio is set (and width/height will fill the ui it is in).
|
||||
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
|
||||
self.view_aspect = Some(view_aspect);
|
||||
self
|
||||
}
|
||||
|
||||
/// Width of plot. By default a plot will fill the ui it is in.
|
||||
/// If you set [`Self::view_aspect`], the width can be calculated from the height.
|
||||
pub fn width(mut self, width: f32) -> Self {
|
||||
self.min_size.x = width;
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
/// Height of plot. By default a plot will fill the ui it is in.
|
||||
/// If you set [`Self::view_aspect`], the height can be calculated from the width.
|
||||
pub fn height(mut self, height: f32) -> Self {
|
||||
self.min_size.y = height;
|
||||
self.height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
/// Minimum size of the plot view.
|
||||
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
||||
self.min_size = min_size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allow_drag(mut self, allow_drag: bool) -> Self {
|
||||
self.allow_drag = allow_drag;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allow_zoom(mut self, allow_zoom: bool) -> Self {
|
||||
self.allow_zoom = allow_zoom;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a horizontal line.
|
||||
/// Can be useful e.g. to show min/max bounds or similar.
|
||||
/// Always fills the full width of the plot.
|
||||
pub fn hline(mut self, mut hline: HLine) -> Self {
|
||||
if hline.stroke.color == Color32::TRANSPARENT {
|
||||
hline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(hline));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a vertical line.
|
||||
/// Can be useful e.g. to show min/max bounds or similar.
|
||||
/// Always fills the full height of the plot.
|
||||
pub fn vline(mut self, mut vline: VLine) -> Self {
|
||||
if vline.stroke.color == Color32::TRANSPARENT {
|
||||
vline.stroke.color = self.auto_color();
|
||||
}
|
||||
self.items.push(Box::new(vline));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_event(mut self, f: OnEvent) -> Self {
|
||||
self.on_event = Some(f);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<OnEvent> egui::Widget for DragPlot<OnEvent>
|
||||
where
|
||||
OnEvent: FnMut(PlotMsg),
|
||||
{
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let Self {
|
||||
id,
|
||||
mut items,
|
||||
min_auto_bounds,
|
||||
min_size,
|
||||
width,
|
||||
height,
|
||||
data_aspect,
|
||||
view_aspect,
|
||||
legend_config,
|
||||
next_auto_color_idx: _,
|
||||
allow_zoom,
|
||||
allow_drag,
|
||||
margin_fraction,
|
||||
selected: _,
|
||||
on_event,
|
||||
mut lines,
|
||||
axis_names,
|
||||
} = self;
|
||||
let plot_id = ui.make_persistent_id(id);
|
||||
let memory = ui
|
||||
.memory()
|
||||
.id_data
|
||||
.get_mut_or_insert_with(plot_id, || PlotMemory {
|
||||
bounds: min_auto_bounds,
|
||||
auto_bounds: false,
|
||||
hovered_entry: None,
|
||||
hidden_items: Default::default(),
|
||||
min_auto_bounds,
|
||||
})
|
||||
.clone();
|
||||
|
||||
let PlotMemory {
|
||||
mut bounds,
|
||||
mut auto_bounds,
|
||||
mut hovered_entry,
|
||||
mut hidden_items,
|
||||
min_auto_bounds,
|
||||
} = memory;
|
||||
|
||||
// Determine the size of the plot in the UI
|
||||
let size = {
|
||||
let width = width
|
||||
.unwrap_or_else(|| {
|
||||
if let (Some(height), Some(aspect)) = (height, view_aspect) {
|
||||
height * aspect
|
||||
} else {
|
||||
ui.available_size_before_wrap().x
|
||||
}
|
||||
})
|
||||
.at_least(min_size.x);
|
||||
|
||||
let height = height
|
||||
.unwrap_or_else(|| {
|
||||
if let Some(aspect) = view_aspect {
|
||||
width / aspect
|
||||
} else {
|
||||
ui.available_size_before_wrap().y
|
||||
}
|
||||
})
|
||||
.at_least(min_size.y);
|
||||
vec2(width, height)
|
||||
};
|
||||
|
||||
let (rect, response) = ui.allocate_exact_size(size, Sense::click_and_drag());
|
||||
let plot_painter = ui.painter().sub_region(rect);
|
||||
|
||||
plot_painter.add(epaint::RectShape {
|
||||
rect,
|
||||
corner_radius: 2.0,
|
||||
fill: ui.visuals().extreme_bg_color,
|
||||
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
|
||||
});
|
||||
|
||||
// Legend
|
||||
let legend = legend_config
|
||||
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
|
||||
|
||||
// Remove the deselected items.
|
||||
items.retain(|item| !hidden_items.contains(item.name()));
|
||||
lines.retain(|item| !hidden_items.contains(&item.name));
|
||||
|
||||
// Highlight the hovered items.
|
||||
if let Some(hovered_name) = &hovered_entry {
|
||||
items
|
||||
.iter_mut()
|
||||
.filter(|entry| entry.name() == hovered_name)
|
||||
.for_each(|entry| entry.highlight());
|
||||
lines
|
||||
.iter_mut()
|
||||
.filter(|entry| &entry.name == hovered_name)
|
||||
.for_each(|entry| {
|
||||
entry.highlight();
|
||||
});
|
||||
}
|
||||
// Move highlighted items to front.
|
||||
items.sort_by_key(|item| item.highlighted());
|
||||
lines.sort_by_key(|item| item.highlighted());
|
||||
|
||||
// Set bounds automatically based on content.
|
||||
if auto_bounds || !bounds.is_valid() {
|
||||
bounds = min_auto_bounds;
|
||||
items
|
||||
.iter()
|
||||
.for_each(|item| bounds.merge(&item.get_bounds()));
|
||||
lines
|
||||
.iter()
|
||||
.for_each(|item| bounds.merge(&item.get_bounds()));
|
||||
bounds.add_relative_margin(margin_fraction);
|
||||
}
|
||||
// Make sure they are not empty.
|
||||
if !bounds.is_valid() {
|
||||
bounds = Bounds::new_symmetrical(1.0);
|
||||
}
|
||||
|
||||
let mut transform = ScreenTransform::new(rect, bounds, false, false);
|
||||
// Enforce equal aspect ratio.
|
||||
if let Some(data_aspect) = data_aspect {
|
||||
transform.set_aspect(data_aspect as f64);
|
||||
}
|
||||
|
||||
// Zooming
|
||||
if allow_zoom {
|
||||
if let Some(hover_pos) = response.hover_pos() {
|
||||
let zoom_factor = if data_aspect.is_some() {
|
||||
Vec2::splat(ui.input().zoom_delta())
|
||||
} else {
|
||||
ui.input().zoom_delta_2d()
|
||||
};
|
||||
if zoom_factor != Vec2::splat(1.0) {
|
||||
transform.zoom(zoom_factor, hover_pos);
|
||||
auto_bounds = false;
|
||||
}
|
||||
|
||||
let scroll_delta = ui.input().scroll_delta;
|
||||
if scroll_delta != Vec2::ZERO {
|
||||
transform.translate_bounds(-scroll_delta);
|
||||
auto_bounds = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize values from functions.
|
||||
items
|
||||
.iter_mut()
|
||||
.for_each(|item| item.initialize(transform.bounds().range_x()));
|
||||
lines
|
||||
.iter_mut()
|
||||
.for_each(|line| line.initialize(transform.bounds().range_x()));
|
||||
|
||||
let bounds = *transform.bounds();
|
||||
let prepared = DragPlotPrepared {
|
||||
items,
|
||||
lines,
|
||||
show_x: true,
|
||||
show_y: true,
|
||||
show_axes: [true, true],
|
||||
transform,
|
||||
axis_names,
|
||||
};
|
||||
if let Some(mut f) = on_event {
|
||||
if let Some(pointer) = response.hover_pos() {
|
||||
if response.mouse_down(PointerButton::Primary) {
|
||||
if let Some(idx) = prepared.find_clicked(pointer) {
|
||||
f(PlotMsg::Clicked(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
if allow_drag && response.dragged_by(PointerButton::Primary) {
|
||||
let delta = response.drag_delta() * Vec2::new(0.18, -0.18);
|
||||
f(PlotMsg::Drag(delta));
|
||||
}
|
||||
}
|
||||
|
||||
prepared.ui(ui, &response);
|
||||
|
||||
if let Some(mut legend) = legend {
|
||||
ui.add(&mut legend);
|
||||
hidden_items = legend.get_hidden_items();
|
||||
hovered_entry = legend.get_hovered_entry_name();
|
||||
}
|
||||
|
||||
ui.memory().id_data.insert(
|
||||
plot_id,
|
||||
PlotMemory {
|
||||
bounds,
|
||||
auto_bounds,
|
||||
hovered_entry,
|
||||
hidden_items,
|
||||
min_auto_bounds,
|
||||
},
|
||||
);
|
||||
response.on_hover_cursor(CursorIcon::Crosshair)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PointerExt {
|
||||
fn mouse_down(&self, pointer: PointerButton) -> bool;
|
||||
}
|
||||
|
||||
impl PointerExt for Response {
|
||||
fn mouse_down(&self, p: PointerButton) -> bool {
|
||||
let pointer = &self.ctx.input().pointer;
|
||||
match p {
|
||||
PointerButton::Primary => pointer.primary_down(),
|
||||
PointerButton::Secondary => pointer.secondary_down(),
|
||||
PointerButton::Middle => pointer.middle_down(),
|
||||
}
|
||||
}
|
||||
}
|
265
amdguid/src/widgets/drag_plot_prepared.rs
Normal file
265
amdguid/src/widgets/drag_plot_prepared.rs
Normal file
@ -0,0 +1,265 @@
|
||||
use egui::{emath, pos2, remap_clamp, vec2, Align2, Layout, NumExt, Pos2, Response, Ui};
|
||||
use epaint::{Color32, Rgba, Shape, Stroke, TextStyle};
|
||||
|
||||
use crate::items::Value;
|
||||
use crate::items::{Line, PlotItem};
|
||||
use crate::transform::ScreenTransform;
|
||||
|
||||
pub struct DragPlotPrepared {
|
||||
pub items: Vec<Box<dyn PlotItem>>,
|
||||
pub lines: Vec<Line>,
|
||||
pub show_x: bool,
|
||||
pub show_y: bool,
|
||||
pub show_axes: [bool; 2],
|
||||
pub transform: ScreenTransform,
|
||||
pub axis_names: [String; 2],
|
||||
}
|
||||
|
||||
impl DragPlotPrepared {
|
||||
pub fn ui(self, ui: &mut Ui, response: &Response) {
|
||||
let mut shapes = Vec::new();
|
||||
|
||||
for d in 0..2 {
|
||||
if self.show_axes[d] {
|
||||
self.paint_axis(ui, d, &mut shapes);
|
||||
}
|
||||
}
|
||||
|
||||
let transform = &self.transform;
|
||||
|
||||
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
||||
plot_ui.set_clip_rect(*transform.frame());
|
||||
for item in &self.items {
|
||||
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
||||
}
|
||||
for item in &self.lines {
|
||||
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
||||
}
|
||||
|
||||
if let Some(pointer) = response.hover_pos() {
|
||||
self.hover(ui, pointer, &mut shapes);
|
||||
}
|
||||
|
||||
ui.painter().sub_region(*transform.frame()).extend(shapes);
|
||||
}
|
||||
|
||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||
let Self { transform, .. } = self;
|
||||
|
||||
let bounds = transform.bounds();
|
||||
let text_style = TextStyle::Body;
|
||||
|
||||
let base: i64 = 10;
|
||||
let base_f = base as f64;
|
||||
|
||||
let min_line_spacing_in_points = 6.0;
|
||||
let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points;
|
||||
let step_size = base_f.powi(step_size.abs().log(base_f).ceil() as i32);
|
||||
|
||||
let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() as f32;
|
||||
|
||||
// Where on the cross-dimension to show the label values
|
||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
||||
|
||||
for i in 0.. {
|
||||
let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor();
|
||||
if value_main > bounds.max[axis] {
|
||||
break;
|
||||
}
|
||||
|
||||
let value = if axis == 0 {
|
||||
Value::new(value_main, value_cross)
|
||||
} else {
|
||||
Value::new(value_cross, value_main)
|
||||
};
|
||||
let pos_in_gui = transform.position_from_value(&value);
|
||||
|
||||
let n = (value_main / step_size).round() as i64;
|
||||
let spacing_in_points = if n % (base * base) == 0 {
|
||||
step_size_in_points * (base_f * base_f) as f32 // think line (multiple of 100)
|
||||
} else if n % base == 0 {
|
||||
step_size_in_points * base_f as f32 // medium line (multiple of 10)
|
||||
} else {
|
||||
step_size_in_points // thin line
|
||||
};
|
||||
|
||||
let line_alpha = remap_clamp(
|
||||
spacing_in_points,
|
||||
(min_line_spacing_in_points as f32)..=300.0,
|
||||
0.0..=0.15,
|
||||
);
|
||||
|
||||
if line_alpha > 0.0 {
|
||||
let line_color = color_from_alpha(ui, line_alpha);
|
||||
|
||||
let mut p0 = pos_in_gui;
|
||||
let mut p1 = pos_in_gui;
|
||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
||||
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
|
||||
}
|
||||
|
||||
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
|
||||
|
||||
if text_alpha > 0.0 {
|
||||
let color = color_from_alpha(ui, text_alpha);
|
||||
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
|
||||
|
||||
let galley = ui.painter().layout_no_wrap(text, text_style, color);
|
||||
|
||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
||||
|
||||
// Make sure we see the labels, even if the axis is off-screen:
|
||||
text_pos[1 - axis] = text_pos[1 - axis]
|
||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
||||
|
||||
shapes.push(Shape::galley(text_pos, galley));
|
||||
}
|
||||
}
|
||||
|
||||
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
|
||||
if ui.visuals().dark_mode {
|
||||
Rgba::from_white_alpha(alpha).into()
|
||||
} else {
|
||||
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_clicked(&self, pointer: Pos2) -> Option<usize> {
|
||||
let Self {
|
||||
transform, lines, ..
|
||||
} = self;
|
||||
let interact_radius: f32 = 16.0;
|
||||
let mut closest_value = None;
|
||||
let mut closest_dist_sq = interact_radius.powi(2);
|
||||
for item in lines {
|
||||
if let Some(values) = item.values() {
|
||||
for (idx, value) in values.values.iter().enumerate() {
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
closest_value = Some(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closest_value
|
||||
}
|
||||
|
||||
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) {
|
||||
let Self {
|
||||
transform,
|
||||
show_x,
|
||||
show_y,
|
||||
items,
|
||||
lines,
|
||||
..
|
||||
} = self;
|
||||
|
||||
if !show_x && !show_y {
|
||||
return;
|
||||
}
|
||||
|
||||
let interact_radius: f32 = 16.0;
|
||||
let mut closest_value = None;
|
||||
let mut closest_item = None;
|
||||
let mut closest_dist_sq = interact_radius.powi(2);
|
||||
for item in items {
|
||||
if let Some(values) = item.values() {
|
||||
for value in &values.values {
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
closest_value = Some(value);
|
||||
closest_item = Some(item.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for item in lines {
|
||||
if let Some(values) = item.values() {
|
||||
for value in &values.values {
|
||||
let pos = transform.position_from_value(value);
|
||||
let dist_sq = pointer.distance_sq(pos);
|
||||
if dist_sq < closest_dist_sq {
|
||||
closest_dist_sq = dist_sq;
|
||||
closest_value = Some(value);
|
||||
closest_item = Some(&item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut prefix = String::new();
|
||||
if let Some(name) = closest_item {
|
||||
if !name.is_empty() {
|
||||
prefix = format!("{}\n", name);
|
||||
}
|
||||
}
|
||||
|
||||
let line_color = if ui.visuals().dark_mode {
|
||||
Color32::from_gray(100).additive()
|
||||
} else {
|
||||
Color32::from_black_alpha(180)
|
||||
};
|
||||
|
||||
let value = if let Some(value) = closest_value {
|
||||
let position = transform.position_from_value(value);
|
||||
shapes.push(Shape::circle_filled(position, 3.0, line_color));
|
||||
*value
|
||||
} else {
|
||||
transform.value_from_position(pointer)
|
||||
};
|
||||
let pointer = transform.position_from_value(&value);
|
||||
|
||||
let rect = transform.frame();
|
||||
|
||||
if *show_x {
|
||||
// vertical line
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
|
||||
(1.0, line_color),
|
||||
));
|
||||
}
|
||||
if *show_y {
|
||||
// horizontal line
|
||||
shapes.push(Shape::line_segment(
|
||||
[pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
|
||||
(1.0, line_color),
|
||||
));
|
||||
}
|
||||
|
||||
let text = {
|
||||
let scale = transform.dvalue_dpos();
|
||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let [x_name, y_name] = &self.axis_names;
|
||||
if *show_x && *show_y {
|
||||
format!(
|
||||
"{}{} = {:.*}\n{} = {:.*}",
|
||||
prefix, x_name, x_decimals, value.x, y_name, y_decimals, value.y
|
||||
)
|
||||
} else if *show_x {
|
||||
format!("{}{} = {:.*}", prefix, x_name, x_decimals, value.x)
|
||||
} else if *show_y {
|
||||
format!("{}{} = {:.*}", prefix, y_name, y_decimals, value.y)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
shapes.push(Shape::text(
|
||||
ui.fonts(),
|
||||
pointer + vec2(3.0, -2.0),
|
||||
Align2::LEFT_BOTTOM,
|
||||
text,
|
||||
TextStyle::Body,
|
||||
ui.visuals().text_color(),
|
||||
));
|
||||
}
|
||||
}
|
150
amdguid/src/widgets/legend.rs
Normal file
150
amdguid/src/widgets/legend.rs
Normal file
@ -0,0 +1,150 @@
|
||||
use std::string::String;
|
||||
|
||||
use egui::{pos2, vec2, Align, PointerButton, Rect, Response, Sense, WidgetInfo, WidgetType};
|
||||
use epaint::{Color32, TextStyle};
|
||||
|
||||
/// Where to place the plot legend.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Corner {
|
||||
LeftTop,
|
||||
RightTop,
|
||||
LeftBottom,
|
||||
RightBottom,
|
||||
}
|
||||
|
||||
impl Corner {
|
||||
pub fn all() -> impl Iterator<Item = Corner> {
|
||||
[
|
||||
Corner::LeftTop,
|
||||
Corner::RightTop,
|
||||
Corner::LeftBottom,
|
||||
Corner::RightBottom,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration for a plot legend.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Legend {
|
||||
pub text_style: TextStyle,
|
||||
pub background_alpha: f32,
|
||||
pub position: Corner,
|
||||
}
|
||||
|
||||
impl Default for Legend {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text_style: TextStyle::Body,
|
||||
background_alpha: 0.75,
|
||||
position: Corner::RightTop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Legend {
|
||||
/// Which text style to use for the legend. Default: `TextStyle::Body`.
|
||||
pub fn text_style(mut self, style: TextStyle) -> Self {
|
||||
self.text_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// The alpha of the legend background. Default: `0.75`.
|
||||
pub fn background_alpha(mut self, alpha: f32) -> Self {
|
||||
self.background_alpha = alpha;
|
||||
self
|
||||
}
|
||||
|
||||
/// In which corner to place the legend. Default: `Corner::RightTop`.
|
||||
pub fn position(mut self, corner: Corner) -> Self {
|
||||
self.position = corner;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LegendEntry {
|
||||
pub color: Color32,
|
||||
pub checked: bool,
|
||||
pub hovered: bool,
|
||||
}
|
||||
|
||||
impl LegendEntry {
|
||||
pub fn new(color: Color32, checked: bool) -> Self {
|
||||
Self {
|
||||
color,
|
||||
checked,
|
||||
hovered: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) -> Response {
|
||||
let Self {
|
||||
color,
|
||||
checked,
|
||||
hovered,
|
||||
} = self;
|
||||
|
||||
let galley =
|
||||
ui.fonts()
|
||||
.layout_delayed_color(text, ui.style().body_text_style, f32::INFINITY);
|
||||
|
||||
let icon_size = galley.size().y;
|
||||
let icon_spacing = icon_size / 5.0;
|
||||
let total_extra = vec2(icon_size + icon_spacing, 0.0);
|
||||
|
||||
let desired_size = total_extra + galley.size();
|
||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
||||
|
||||
response
|
||||
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
|
||||
|
||||
let visuals = ui.style().interact(&response);
|
||||
let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT;
|
||||
|
||||
let icon_position_x = if label_on_the_left {
|
||||
rect.right() - icon_size / 2.0
|
||||
} else {
|
||||
rect.left() + icon_size / 2.0
|
||||
};
|
||||
let icon_position = pos2(icon_position_x, rect.center().y);
|
||||
let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size));
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.add(epaint::CircleShape {
|
||||
center: icon_rect.center(),
|
||||
radius: icon_size * 0.5,
|
||||
fill: visuals.bg_fill,
|
||||
stroke: visuals.bg_stroke,
|
||||
});
|
||||
|
||||
if *checked {
|
||||
let fill = if *color == Color32::TRANSPARENT {
|
||||
ui.visuals().noninteractive().fg_stroke.color
|
||||
} else {
|
||||
*color
|
||||
};
|
||||
painter.add(epaint::Shape::circle_filled(
|
||||
icon_rect.center(),
|
||||
icon_size * 0.4,
|
||||
fill,
|
||||
));
|
||||
}
|
||||
|
||||
let text_position_x = if label_on_the_left {
|
||||
rect.right() - icon_size - icon_spacing - galley.size().x
|
||||
} else {
|
||||
rect.left() + icon_size + icon_spacing
|
||||
};
|
||||
|
||||
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y);
|
||||
painter.galley_with_color(text_position, galley, visuals.text_color());
|
||||
|
||||
*checked ^= response.clicked_by(PointerButton::Primary);
|
||||
*hovered = response.hovered();
|
||||
|
||||
response
|
||||
}
|
||||
}
|
113
amdguid/src/widgets/legend_widget.rs
Normal file
113
amdguid/src/widgets/legend_widget.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use egui::{vec2, Align, Direction, Frame, Layout, Rect, Response, Ui, Widget};
|
||||
use epaint::ahash::AHashSet;
|
||||
use epaint::Color32;
|
||||
|
||||
use crate::items::PlotItem;
|
||||
use crate::widgets::legend::{Corner, Legend, LegendEntry};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LegendWidget {
|
||||
rect: Rect,
|
||||
entries: BTreeMap<String, LegendEntry>,
|
||||
config: Legend,
|
||||
}
|
||||
|
||||
impl LegendWidget {
|
||||
/// Create a new legend from items, the names of items that are hidden and the style of the
|
||||
/// text. Returns `None` if the legend has no entries.
|
||||
pub fn try_new(
|
||||
rect: Rect,
|
||||
config: Legend,
|
||||
items: &[Box<dyn PlotItem>],
|
||||
hidden_items: &AHashSet<String>,
|
||||
) -> Option<Self> {
|
||||
let mut entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
|
||||
items
|
||||
.iter()
|
||||
.filter(|item| !item.name().is_empty())
|
||||
.for_each(|item| {
|
||||
entries
|
||||
.entry(item.name().to_string())
|
||||
.and_modify(|entry| {
|
||||
if entry.color != item.color() {
|
||||
// Multiple items with different colors
|
||||
entry.color = Color32::TRANSPARENT;
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| {
|
||||
let color = item.color();
|
||||
let checked = !hidden_items.contains(item.name());
|
||||
LegendEntry::new(color, checked)
|
||||
});
|
||||
});
|
||||
(!entries.is_empty()).then(|| Self {
|
||||
rect,
|
||||
entries,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
// Get the names of the hidden items.
|
||||
pub fn get_hidden_items(&self) -> AHashSet<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|(_, entry)| !entry.checked)
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Get the name of the hovered items.
|
||||
pub fn get_hovered_entry_name(&self) -> Option<String> {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|(_, entry)| entry.hovered)
|
||||
.map(|(name, _)| name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut LegendWidget {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
let LegendWidget {
|
||||
rect,
|
||||
entries,
|
||||
config,
|
||||
} = self;
|
||||
|
||||
let main_dir = match config.position {
|
||||
Corner::LeftTop | Corner::RightTop => Direction::TopDown,
|
||||
Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp,
|
||||
};
|
||||
let cross_align = match config.position {
|
||||
Corner::LeftTop | Corner::LeftBottom => Align::LEFT,
|
||||
Corner::RightTop | Corner::RightBottom => Align::RIGHT,
|
||||
};
|
||||
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align);
|
||||
let legend_pad = 4.0;
|
||||
let legend_rect = rect.shrink(legend_pad);
|
||||
let mut legend_ui = ui.child_ui(legend_rect, layout);
|
||||
legend_ui
|
||||
.scope(|ui| {
|
||||
ui.style_mut().body_text_style = config.text_style;
|
||||
let background_frame = Frame {
|
||||
margin: vec2(8.0, 4.0),
|
||||
corner_radius: ui.style().visuals.window_corner_radius,
|
||||
shadow: epaint::Shadow::default(),
|
||||
fill: ui.style().visuals.extreme_bg_color,
|
||||
stroke: ui.style().visuals.window_stroke(),
|
||||
}
|
||||
.multiply_with_opacity(config.background_alpha);
|
||||
background_frame
|
||||
.show(ui, |ui| {
|
||||
entries
|
||||
.iter_mut()
|
||||
.map(|(name, entry)| entry.ui(ui, name.clone()))
|
||||
.reduce(|r1, r2| r1.union(r2))
|
||||
.unwrap()
|
||||
})
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
}
|
||||
}
|
12
amdguid/src/widgets/mod.rs
Normal file
12
amdguid/src/widgets/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
pub mod change_fan_settings;
|
||||
pub mod config_file;
|
||||
pub mod cooling_performance;
|
||||
pub mod drag_plot;
|
||||
pub mod drag_plot_prepared;
|
||||
pub mod legend;
|
||||
pub mod legend_widget;
|
||||
pub mod reload_section;
|
||||
|
||||
pub use change_fan_settings::*;
|
||||
pub use config_file::*;
|
||||
pub use cooling_performance::*;
|
60
amdguid/src/widgets/reload_section.rs
Normal file
60
amdguid/src/widgets/reload_section.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use crate::app::{ChangeState, FanServices};
|
||||
use amdgpu::helper_cmd::Command;
|
||||
use egui::{PointerButton, Response, Sense, Ui};
|
||||
use epaint::Color32;
|
||||
|
||||
pub struct ReloadSection<'l> {
|
||||
pub services: &'l mut FanServices,
|
||||
}
|
||||
|
||||
impl<'l> egui::Widget for ReloadSection<'l> {
|
||||
fn ui(self, ui: &mut Ui) -> Response {
|
||||
ui.vertical(|ui| {
|
||||
ui.label("Reload config for service");
|
||||
|
||||
self.services.0.iter_mut().for_each(|service| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("PID {}", service.pid.0));
|
||||
if ui.button("Reload").clicked_by(PointerButton::Primary) {
|
||||
service.reload = ChangeState::Reloading;
|
||||
|
||||
match amdgpu::helper_cmd::send_command(Command::ReloadConfig {
|
||||
pid: service.pid,
|
||||
}) {
|
||||
Ok(response) => {
|
||||
service.reload = ChangeState::Success;
|
||||
log::info!("{:?}", response)
|
||||
}
|
||||
Err(e) => {
|
||||
service.reload = ChangeState::Failure(format!("{:?}", e));
|
||||
log::error!("Failed to reload config. {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
match &service.reload {
|
||||
ChangeState::New => {}
|
||||
ChangeState::Reloading => {
|
||||
ui.label("Reloading...");
|
||||
}
|
||||
ChangeState::Success => {
|
||||
ui.add(egui::Label::new("Reloaded").text_color(Color32::DARK_GREEN));
|
||||
}
|
||||
ChangeState::Failure(msg) => {
|
||||
ui.add(
|
||||
egui::Label::new(format!("Failure. {}", msg))
|
||||
.text_color(Color32::RED),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.allocate_response(ui.available_size(), Sense::click())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ReloadSection<'l> {
|
||||
pub fn new(services: &'l mut FanServices) -> Self {
|
||||
Self { services }
|
||||
}
|
||||
}
|
29
amdmond-lib/Cargo.toml
Normal file
29
amdmond-lib/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "amdmond-lib"
|
||||
version = "1.0.9"
|
||||
edition = "2021"
|
||||
description = "AMD GPU monitoring tool for Linux"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["hardware", "amdgpu"]
|
||||
categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["monitor", "fan"] }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
csv = { version = "1.1.6" }
|
||||
|
||||
thiserror = "1.0.30"
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
pretty_env_logger = { version = "0.4.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }
|
26
amdmond-lib/src/errors.rs
Normal file
26
amdmond-lib/src/errors.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use amdgpu::utils;
|
||||
use amdgpu_config::{fan, monitor};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmdMonError {
|
||||
#[error("Mon AMD GPU card was found")]
|
||||
NoHwMon,
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
MonConfigError(#[from] monitor::ConfigError),
|
||||
#[error("{0}")]
|
||||
FanConfigError(#[from] fan::ConfigError),
|
||||
#[error("{0}")]
|
||||
AmdUtils(#[from] utils::AmdGpuError),
|
||||
#[error("{0}")]
|
||||
Csv(#[from] csv::Error),
|
||||
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
|
||||
NonIntTemp(std::num::ParseIntError),
|
||||
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
|
||||
NonIntPwm(std::num::ParseIntError),
|
||||
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
|
||||
InvalidMonitorFormat,
|
||||
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
|
||||
EmptyTempSet,
|
||||
}
|
117
amdmond-lib/src/lib.rs
Normal file
117
amdmond-lib/src/lib.rs
Normal file
@ -0,0 +1,117 @@
|
||||
pub mod errors;
|
||||
|
||||
use crate::errors::AmdMonError;
|
||||
use amdgpu::hw_mon::HwMon;
|
||||
use amdgpu::utils::load_temp_inputs;
|
||||
use amdgpu::{
|
||||
TempInput, PULSE_WIDTH_MODULATION, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN,
|
||||
};
|
||||
use amdgpu_config::fan;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AmdMonError>;
|
||||
|
||||
pub struct AmdMon {
|
||||
temp_input: Option<TempInput>,
|
||||
inputs: Vec<String>,
|
||||
hw_mon: HwMon,
|
||||
/// Minimal modulation (between 0-255)
|
||||
pub pwm_min: Option<u32>,
|
||||
/// Maximal modulation (between 0-255)
|
||||
pub pwm_max: Option<u32>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AmdMon {
|
||||
type Target = HwMon;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.hw_mon
|
||||
}
|
||||
}
|
||||
|
||||
impl AmdMon {
|
||||
pub fn wrap_all(mons: Vec<HwMon>, config: &fan::Config) -> Vec<Self> {
|
||||
mons.into_iter()
|
||||
.map(|hw_mon| Self::wrap(hw_mon, config))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self {
|
||||
Self {
|
||||
temp_input: config.temp_input().cloned(),
|
||||
inputs: load_temp_inputs(&hw_mon),
|
||||
hw_mon,
|
||||
pwm_min: None,
|
||||
pwm_max: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gpu_temp(&self) -> Vec<(String, crate::Result<f64>)> {
|
||||
self.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let temp = self
|
||||
.read_gpu_temp(name.as_str())
|
||||
.map(|temp| temp as f64 / 1000f64);
|
||||
(name, temp)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn gpu_temp_of(&self, input_idx: usize) -> Option<(&String, crate::Result<f64>)> {
|
||||
self.inputs.get(input_idx).map(|name| {
|
||||
let temp = self
|
||||
.read_gpu_temp(name.as_str())
|
||||
.map(|temp| temp as f64 / 1000f64);
|
||||
(name, temp)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
||||
let value = self
|
||||
.hw_mon_read(name)?
|
||||
.parse::<u64>()
|
||||
.map_err(AmdMonError::NonIntTemp)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn pwm(&self) -> crate::Result<u32> {
|
||||
let value = self
|
||||
.hw_mon_read(PULSE_WIDTH_MODULATION)?
|
||||
.parse()
|
||||
.map_err(AmdMonError::NonIntPwm)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn pwm_min(&mut self) -> u32 {
|
||||
if self.pwm_min.is_none() {
|
||||
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
|
||||
};
|
||||
self.pwm_min.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn pwm_max(&mut self) -> u32 {
|
||||
if self.pwm_max.is_none() {
|
||||
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
|
||||
};
|
||||
self.pwm_max.unwrap_or(255)
|
||||
}
|
||||
|
||||
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
|
||||
if let Some(input) = self.temp_input.as_ref() {
|
||||
let value = self.read_gpu_temp(&input.as_string())?;
|
||||
return Ok(value as f64 / 1000f64);
|
||||
}
|
||||
let mut results = Vec::with_capacity(self.inputs.len());
|
||||
for name in self.inputs.iter() {
|
||||
results.push(self.read_gpu_temp(name).unwrap_or(0));
|
||||
}
|
||||
results.sort_unstable();
|
||||
let value = results
|
||||
.last()
|
||||
.copied()
|
||||
.map(|temp| temp as f64 / 1000f64)
|
||||
.ok_or(AmdMonError::EmptyTempSet)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "amdmond"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
edition = "2021"
|
||||
description = "AMD GPU monitoring tool for Linux"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -9,14 +9,15 @@ categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.8" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["monitor", "fan"] }
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["monitor", "fan"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
csv = { version = "1.1.6" }
|
||||
|
||||
thiserror = "1.0.30"
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
@ -27,3 +28,4 @@ pretty_env_logger = { version = "0.4.0" }
|
||||
[dev-dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }
|
||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
||||
|
@ -1,8 +1,4 @@
|
||||
use crate::{log_file, watch, AmdMonError};
|
||||
use amdgpu::hw_mon::HwMon;
|
||||
use amdgpu::utils::load_temp_inputs;
|
||||
use amdgpu::{TempInput, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN};
|
||||
use amdgpu_config::{fan, PULSE_WIDTH_MODULATION};
|
||||
use crate::{log_file, watch};
|
||||
|
||||
#[derive(gumdrop::Options)]
|
||||
pub enum Command {
|
||||
@ -15,100 +11,3 @@ impl Default for Command {
|
||||
Self::Watch(watch::Watch::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AmdMon {
|
||||
temp_input: Option<TempInput>,
|
||||
inputs: Vec<String>,
|
||||
hw_mon: HwMon,
|
||||
/// Minimal modulation (between 0-255)
|
||||
pub pwm_min: Option<u32>,
|
||||
/// Maximal modulation (between 0-255)
|
||||
pub pwm_max: Option<u32>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AmdMon {
|
||||
type Target = HwMon;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.hw_mon
|
||||
}
|
||||
}
|
||||
|
||||
impl AmdMon {
|
||||
pub(crate) fn wrap_all(mons: Vec<HwMon>, config: &fan::Config) -> Vec<Self> {
|
||||
mons.into_iter()
|
||||
.map(|hw_mon| Self::wrap(hw_mon, config))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self {
|
||||
Self {
|
||||
temp_input: config.temp_input().cloned(),
|
||||
inputs: load_temp_inputs(&hw_mon),
|
||||
hw_mon,
|
||||
pwm_min: None,
|
||||
pwm_max: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gpu_temp(&self) -> Vec<(String, crate::Result<f64>)> {
|
||||
self.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let temp = self
|
||||
.read_gpu_temp(name.as_str())
|
||||
.map(|temp| temp as f64 / 1000f64);
|
||||
(name, temp)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
||||
let value = self
|
||||
.hw_mon_read(name)?
|
||||
.parse::<u64>()
|
||||
.map_err(AmdMonError::NonIntTemp)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn pwm(&self) -> crate::Result<u32> {
|
||||
let value = self
|
||||
.hw_mon_read(PULSE_WIDTH_MODULATION)?
|
||||
.parse()
|
||||
.map_err(AmdMonError::NonIntPwm)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn pwm_min(&mut self) -> u32 {
|
||||
if self.pwm_min.is_none() {
|
||||
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
|
||||
};
|
||||
self.pwm_min.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn pwm_max(&mut self) -> u32 {
|
||||
if self.pwm_max.is_none() {
|
||||
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
|
||||
};
|
||||
self.pwm_max.unwrap_or(255)
|
||||
}
|
||||
|
||||
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
|
||||
if let Some(input) = self.temp_input.as_ref() {
|
||||
let value = self.read_gpu_temp(&input.as_string())?;
|
||||
return Ok(value as f64 / 1000f64);
|
||||
}
|
||||
let mut results = Vec::with_capacity(self.inputs.len());
|
||||
for name in self.inputs.iter() {
|
||||
results.push(self.read_gpu_temp(name).unwrap_or(0));
|
||||
}
|
||||
results.sort_unstable();
|
||||
let value = results
|
||||
.last()
|
||||
.copied()
|
||||
.map(|temp| temp as f64 / 1000f64)
|
||||
.ok_or(AmdMonError::EmptyTempSet)?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::command::AmdMon;
|
||||
use crate::AmdMonError;
|
||||
use amdgpu::utils::hw_mons;
|
||||
use amdgpu_config::fan;
|
||||
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
|
||||
use amdgpu_config::monitor::Config;
|
||||
use amdmond_lib::errors::AmdMonError;
|
||||
use amdmond_lib::AmdMon;
|
||||
|
||||
#[derive(gumdrop::Options)]
|
||||
pub struct LogFile {
|
||||
@ -24,7 +24,7 @@ struct Stat {
|
||||
temperature_setting: f64,
|
||||
}
|
||||
|
||||
pub fn run(command: LogFile, config: Config) -> crate::Result<()> {
|
||||
pub fn run(command: LogFile, config: Config) -> amdmond_lib::Result<()> {
|
||||
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
|
||||
|
||||
let duration = std::time::Duration::new(
|
||||
|
@ -2,39 +2,11 @@ mod command;
|
||||
mod log_file;
|
||||
mod watch;
|
||||
|
||||
use amdgpu::utils;
|
||||
use gumdrop::Options;
|
||||
|
||||
use crate::command::Command;
|
||||
use amdgpu::utils::ensure_config_dir;
|
||||
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
|
||||
use amdgpu_config::{fan, monitor};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmdMonError {
|
||||
#[error("Mon AMD GPU card was found")]
|
||||
NoHwMon,
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
MonConfigError(#[from] monitor::ConfigError),
|
||||
#[error("{0}")]
|
||||
FanConfigError(#[from] fan::ConfigError),
|
||||
#[error("{0}")]
|
||||
AmdUtils(#[from] utils::AmdGpuError),
|
||||
#[error("{0}")]
|
||||
Csv(#[from] csv::Error),
|
||||
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
|
||||
NonIntTemp(std::num::ParseIntError),
|
||||
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
|
||||
NonIntPwm(std::num::ParseIntError),
|
||||
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
|
||||
InvalidMonitorFormat,
|
||||
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
|
||||
EmptyTempSet,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AmdMonError>;
|
||||
|
||||
#[derive(gumdrop::Options)]
|
||||
pub struct Opts {
|
||||
@ -48,7 +20,7 @@ pub struct Opts {
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
fn run(config: Config) -> Result<()> {
|
||||
fn run(config: Config) -> amdmond_lib::Result<()> {
|
||||
let opts: Opts = Opts::parse_args_default_or_exit();
|
||||
|
||||
if opts.version {
|
||||
@ -65,7 +37,7 @@ fn run(config: Config) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn setup() -> Result<(String, Config)> {
|
||||
fn setup() -> amdmond_lib::Result<(String, Config)> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "DEBUG");
|
||||
}
|
||||
@ -81,7 +53,7 @@ fn setup() -> Result<(String, Config)> {
|
||||
Ok((config_path, config))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
fn main() -> amdmond_lib::Result<()> {
|
||||
let (_config_path, config) = match setup() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
|
@ -3,9 +3,8 @@ use std::str::FromStr;
|
||||
use amdgpu::utils::{hw_mons, linear_map};
|
||||
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
|
||||
use amdgpu_config::{fan, monitor};
|
||||
|
||||
use crate::command::AmdMon;
|
||||
use crate::AmdMonError;
|
||||
use amdmond_lib::errors::AmdMonError;
|
||||
use amdmond_lib::AmdMon;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MonitorFormat {
|
||||
@ -49,7 +48,7 @@ impl Default for Watch {
|
||||
}
|
||||
|
||||
/// Start print cards temperature and fan speed
|
||||
pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> {
|
||||
pub fn run(monitor: Watch, _config: monitor::Config) -> amdmond_lib::Result<()> {
|
||||
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
|
||||
match monitor.format {
|
||||
MonitorFormat::Short => short(fan_config),
|
||||
@ -57,7 +56,7 @@ pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verbose(config: fan::Config) -> crate::Result<()> {
|
||||
pub fn verbose(config: fan::Config) -> amdmond_lib::Result<()> {
|
||||
let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config);
|
||||
|
||||
loop {
|
||||
@ -100,7 +99,7 @@ pub fn verbose(config: fan::Config) -> crate::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short(config: fan::Config) -> crate::Result<()> {
|
||||
pub fn short(config: fan::Config) -> amdmond_lib::Result<()> {
|
||||
let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config);
|
||||
loop {
|
||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||
|
7
amdvold/.cargo/config.toml
Normal file
7
amdvold/.cargo/config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[build]
|
||||
target = "x86_64-unknown-linux-musl"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "amdvold"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
edition = "2018"
|
||||
description = "AMDGPU fan control service"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@ -9,12 +9,12 @@ categories = ["hardware-support"]
|
||||
repository = "https://github.com/Eraden/amdgpud"
|
||||
|
||||
[dependencies]
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.8" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["voltage"] }
|
||||
amdgpu = { path = "../amdgpu", version = "1.0.9" }
|
||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["voltage"] }
|
||||
|
||||
serde = { version = "1.0.126", features = ["derive"] }
|
||||
toml = { version = "0.5.8" }
|
||||
thiserror = "1.0.30"
|
||||
thiserror = { version = "1.0.30" }
|
||||
gumdrop = { version = "0.8.0" }
|
||||
|
||||
log = { version = "0.4.14" }
|
||||
|
BIN
assets/config.png
Normal file
BIN
assets/config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 871 KiB |
BIN
assets/monitoring.png
Normal file
BIN
assets/monitoring.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 860 KiB |
BIN
assets/settings.png
Normal file
BIN
assets/settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 816 KiB |
11
build.sh
11
build.sh
@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
cargo build --release
|
||||
|
||||
strip target/x86_64-unknown-linux-musl/release/amdfand
|
||||
strip target/x86_64-unknown-linux-musl/release/amdvold
|
||||
strip target/x86_64-unknown-linux-musl/release/amdmond
|
||||
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond
|
23
scripts/build.sh
Executable file
23
scripts/build.sh
Executable file
@ -0,0 +1,23 @@
|
||||
set -e +x
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
./scripts/compile.sh
|
||||
|
||||
strip target/x86_64-unknown-linux-musl/release/amdfand
|
||||
strip target/x86_64-unknown-linux-musl/release/amdvold
|
||||
strip target/x86_64-unknown-linux-musl/release/amdmond
|
||||
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold
|
||||
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond
|
||||
|
||||
cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features xorg
|
||||
strip target/x86_64-unknown-linux-gnu/release/amdguid
|
||||
upx --best --lzma target/x86_64-unknown-linux-gnu/release/amdguid
|
||||
zip ./target/amdguid-glium.zip ./target/x86_64-unknown-linux-gnu/release/amdguid
|
||||
|
||||
cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features wayland
|
||||
strip target/x86_64-unknown-linux-gnu/release/amdguid
|
||||
upx --best --lzma target/x86_64-unknown-linux-gnu/release/amdguid
|
||||
zip ./target/amdguid-wayland.zip ./target/x86_64-unknown-linux-gnu/release/amdguid
|
9
scripts/compile.sh
Executable file
9
scripts/compile.sh
Executable file
@ -0,0 +1,9 @@
|
||||
set -e +x
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
cargo build --release --target x86_64-unknown-linux-musl --bin amdfand
|
||||
cargo build --release --target x86_64-unknown-linux-musl --bin amdmond
|
||||
cargo build --release --target x86_64-unknown-linux-musl --bin amdvold
|
||||
cargo build --release --target x86_64-unknown-linux-musl --bin amdgui-helper
|
||||
cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features xorg
|
@ -14,5 +14,15 @@ cargo publish
|
||||
cd $(git_root)/amdvold
|
||||
cargo publish
|
||||
|
||||
cd $(git_root)/amdmond-lib
|
||||
cargo publish
|
||||
|
||||
cd $(git_root)/amdmond
|
||||
cargo publish
|
||||
|
||||
cd $(git_root)/amdgui-helper
|
||||
cargo publish
|
||||
|
||||
|
||||
cd $(git_root)/amdguid
|
||||
cargo publish
|
8
scripts/zip-ci.sh
Executable file
8
scripts/zip-ci.sh
Executable file
@ -0,0 +1,8 @@
|
||||
echo Building binaries-$1.zip
|
||||
|
||||
zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdfand;
|
||||
zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdmond;
|
||||
zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdvold;
|
||||
zip binaries-$1.zip ./target/x86_64-unknown-linux-musl/release/amdgui-helper;
|
||||
zip binaries-$1.zip ./target/amdguid-wayland.zip;
|
||||
zip binaries-$1.zip ./target/amdguid-glium.zip
|
@ -5,5 +5,6 @@ After=sysinit.target local-fs.target
|
||||
Restart=on-failure
|
||||
RestartSec=4
|
||||
ExecStart=/usr/bin/amdfand service
|
||||
Environment=RUST_LOG=ERROR
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
12
services/amdgui-helper
Executable file
12
services/amdgui-helper
Executable file
@ -0,0 +1,12 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
description="amdgui helper service"
|
||||
pidfile="/run/${SVCNAME}.pid"
|
||||
command="/usr/bin/amdgui-helper"
|
||||
command_args="service"
|
||||
command_user="root"
|
||||
command_background=true
|
||||
|
||||
depend() {
|
||||
need udev
|
||||
}
|
10
services/amdgui-helper.service
Normal file
10
services/amdgui-helper.service
Normal file
@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=AMD GPU gui helper
|
||||
After=sysinit.target local-fs.target
|
||||
[Service]
|
||||
Restart=on-failure
|
||||
RestartSec=4
|
||||
ExecStart=/usr/bin/amdgui-helper
|
||||
Environment=RUST_LOG=ERROR
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -5,5 +5,6 @@ After=sysinit.target local-fs.target
|
||||
Restart=on-failure
|
||||
RestartSec=4
|
||||
ExecStart=/usr/bin/amdmond log_file -s /var/log/amdmon.csv
|
||||
Environment=RUST_LOG=ERROR
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@ -5,5 +5,6 @@ After=sysinit.target local-fs.target
|
||||
Restart=on-failure
|
||||
RestartSec=4
|
||||
ExecStart=/usr/bin/amdvold service
|
||||
Environment=RUST_LOG=ERROR
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
Loading…
Reference in New Issue
Block a user