Merge pull request #32 from Eraden/gui-app

GUI application
This commit is contained in:
Adrian Woźniak 2022-02-09 19:48:01 +01:00 committed by GitHub
commit 14b9a694fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 6994 additions and 213 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,11 @@
[workspace]
members = ["amdgpu", "amdgpu-config", "amdfand", "amdvold", "amdmond"]
members = [
"amdgpu",
"amdgpu-config",
"amdfand",
"amdvold",
"amdmond",
"amdmond-lib",
"amdguid",
"amdgui-helper",
]

View File

@ -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.

View File

@ -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"] }

View File

@ -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

View 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));

View File

@ -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)
}

View File

@ -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()

View File

@ -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" }

View File

@ -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
View 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());
}
}

View File

@ -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")]

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,7 @@
[build]
target = "x86_64-unknown-linux-musl"
[profile.release]
lto = true
panic = "abort"
codegen-units = 1

View File

@ -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" }

View File

@ -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
View 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)
}

View File

@ -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| {

View File

@ -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
View 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(())
}
}

View File

@ -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

View File

@ -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)?;

View File

@ -0,0 +1,7 @@
[build]
target = "x86_64-unknown-linux-musl"
[profile.release]
lto = true
panic = "abort"
codegen-units = 1

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,7 @@
[build]
target = "x86_64-unknown-linux-gnu"
[profile.release]
lto = true
panic = "abort"
codegen-units = 1

View 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
View 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
View 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
View 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;
}
}
}

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
}
}

View 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()
}
}

View 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
}
}

View 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
View 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()
}
}

View 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
View 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
View 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
}
}

View 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
View 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
View 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
View 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);
}
}
}

View 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;
}
_ => {}
}
}
}

View 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
}
}

View 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);
}
}

View 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(),
}
}
}

View 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(),
));
}
}

View 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
}
}

View 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
}
}

View 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::*;

View 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
View 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
View 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
View 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)
}
}

View File

@ -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" }

View File

@ -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)
}
}

View File

@ -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(

View File

@ -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) => {

View File

@ -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);

View File

@ -0,0 +1,7 @@
[build]
target = "x86_64-unknown-linux-musl"
[profile.release]
lto = true
panic = "abort"
codegen-units = 1

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

BIN
assets/monitoring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

BIN
assets/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

View File

@ -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
View 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
View 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

View File

@ -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
View 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

View File

@ -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
View 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
}

View 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

View File

@ -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

View File

@ -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