Start GUI

GUI in iced

Basic plot manipulation

Working solution

Fix documentation

Fix build scripts

Slice application

Change views

Very basic GUI

Reload config

GUI Helper for root tasks - maybe sock files for all services will be better

Add save button

Xorg gui and save config to target file

Documentation and clippy fixes

Avoid compiling gui on CI

Readme files

Add missing dependencies

Add missing pgp key

Refactor workflow

Refactor workflow

Add drag and drop
This commit is contained in:
eraden 2021-12-04 09:02:57 +01:00 committed by Adrian Woźniak
parent de5f2b77cb
commit 7616ddd8fc
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
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