Compare commits

..

No commits in common. "main" and "add_temp_input" have entirely different histories.

136 changed files with 988 additions and 12864 deletions

View File

@ -10,119 +10,32 @@ env:
CARGO_TERM_COLOR: always
jobs:
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
- name: Add target
run: rustup target install x86_64-unknown-linux-musl
- 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 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 ]
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 archive with all
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}.tar.gz
path: ./tmp/${{ matrix.os }}.tar.gz
- name: Upload amdfand
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdfand.tar.gz
path: ./tmp/amdfand.tar.gz
- name: Upload amdguid-glium
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdguid-glium.tar.gz
path: ./tmp/amdguid-glium.tar.gz
- name: Upload amdguid-glow
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdguid-glow.tar.gz
path: ./tmp/amdguid-glow.tar.gz
- name: Upload amdguid-wayland
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdguid-wayland.tar.gz
path: ./tmp/amdguid-wayland.tar.gz
- name: Upload amdmond
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdmond.tar.gz
path: ./tmp/amdmond.tar.gz
- name: Upload amdvold
uses: actions/upload-artifact@v2.2.4
with:
name: ${{ matrix.os }}-amdvold.tar.gz
path: ./tmp/amdvold.tar.gz
- uses: actions/checkout@v2
- 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
- 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
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.4
with:
# Artifact name
name: amdfand-${{ matrix.os }}
# A file, directory or wildcard pattern that describes what to upload
path: ./target/x86_64-unknown-linux-musl/release/amdfand
# The desired behavior if no files are found using the provided path.

3252
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,18 @@
[workspace]
members = [
"amdgpu",
"amdgpu-config",
"amdfan",
"amdfand",
"amdvold",
"amdmond",
"amdmond-lib",
"amdguid",
"amdgui-helper",
"amdportsd",
]
[package]
name = "amdfand"
version = "1.0.5"
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]
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
gumdrop = { version = "0.8.0" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }

View File

@ -2,15 +2,20 @@ MIT License
Copyright (c) 2021 Adrian Woźniak
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

151
README.md
View File

@ -1,113 +1,74 @@
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
[![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=discord)](https://discord.gg/HXN2QXj3Gv)
[![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=buymeacoffee)](https://discord.gg/HXN2QXj3Gv)
[![Buy me a coffee](https://static.ita-prog.pl/buy-me-a-coffee-64x64.png) This project is created with love. Please, support me](https://www.buymeacoffee.com/eraden)
# AMDGPU Fan control service
# AMD GPU management tools
Available commands:
This repository holds couple tools for AMD graphic cards
* `monitor` - Print current temp and fan speed
* `service` - Set fan speed depends on GPU temperature
* `set-automatic` - Switch to GPU automatic fan speed control
* `set-manual` - Switch to GPU manual fan speed control
* `available` - Print available cards
* `amdfand` - fan speed daemon (MUSL)
* `amdvold` - voltage and overclocking tool (MUSL)
* `amdmond` - monitor daemon (MUSL)
* `amdguid` - GUI manager (GLIBC)
* `amdgui-helper` - daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files (MUSL)
#### amdfand set-automatic | set-manual [OPTIONS]
For more information please check README each of them.
Optional arguments:
## Roadmap
* -h, --help Help message
* -c, --card CARD GPU Card number
* [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
* [X] GUI application using native Rust framework (ex. egui, druid)
## License
This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work.
## Supported OS
Only Linux is supported.
BSD OS may work if compiled from source, but it wasn't tested.
I also don't know how to enable all amd gpu features on BSD.
### Officially supported
* Arch Linux
* Ubuntu 18.04
* Ubuntu 20.04
### Other
It should be possible to run MUSL programs on any Linux distribution.
GLIBC applications depends on shared glibc libraries and version of this library MUST match.
If you have other version you may download linked version and place it in application directory.
Or you can compile it from source
#### Compile
## Usage
```bash
./scripts/build.sh
cargo install argonfand
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
```
#### Download missing shared libraries
## Config file
##### Check linked versions
```toml
# /etc/amdfand/config.toml
log_level = "Error"
cards = ["card0"]
```bash
ldd ./amdguid
[[speed_matrix]]
temp = 4.0
speed = 4.0
[[speed_matrix]]
temp = 30.0
speed = 33.0
[[speed_matrix]]
temp = 45.0
speed = 50.0
[[speed_matrix]]
temp = 60.0
speed = 66.0
[[speed_matrix]]
temp = 65.0
speed = 69.0
[[speed_matrix]]
temp = 70.0
speed = 75.0
[[speed_matrix]]
temp = 75.0
speed = 89.0
[[speed_matrix]]
temp = 80.0
speed = 100.0
```
Example output:
## :bookmark: License
```
linux-vdso.so.1 (0x00007ffd706df000)
libxcb.so.1 => /usr/lib/libxcb.so.1 (0x00007f4254a50000)
libxcb-render.so.0 => /usr/lib/libxcb-render.so.0 (0x00007f4254a40000)
libxcb-shape.so.0 => /usr/lib/libxcb-shape.so.0 (0x00007f4254a3b000)
libxcb-xfixes.so.0 => /usr/lib/libxcb-xfixes.so.0 (0x00007f4254a31000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f4254a2c000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f4254a11000)
librt.so.1 => /usr/lib/librt.so.1 (0x00007f4254a0a000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f4254a05000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f425491d000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f4254713000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f42556a6000)
libXau.so.6 => /usr/lib/libXau.so.6 (0x00007f425470e000)
libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0x00007f4254706000)
```
This work is dual-licensed under Apache 2.0 and MIT.
You can choose between one of them if you use this work.
If anything is missing you may download is and place it side-by-side with binary
##### Example:
```
opt/
├─ amdguid/
│ ├─ amdguid
│ ├─ shared_lib_a/
│ ├─ shared_lib_b/
usr/
├─ bin/
│ ├─ amdguid
```
Where:
* `/opt/amdguid/amdguid` is binary file
* `/usr/bin/amdguid` is shell script with following content
```bash
#!/usr/bin/env bash
cd /opt/amdguid
./amdguid
```
`SPDX-License-Identifier: Apache-2.0 OR MIT`

View File

@ -1,26 +0,0 @@
[package]
name = "amdfan"
version = "0.1.0"
edition = "2021"
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
ron = { version = "0.1" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
tui = { version = "0.18.0", features = [] }
crossbeam = { version = "0.8.1" }
crossterm = { version = "0.23.2" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }

View File

@ -1,315 +0,0 @@
use std::io;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use amdgpu_config::fan::{MatrixPoint, DEFAULT_FAN_CONFIG_PATH};
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use gumdrop::Options;
use tui::backend::{Backend, CrosstermBackend};
use tui::layout::*;
use tui::style::{Color, Modifier, Style};
use tui::symbols::Marker;
use tui::widgets::*;
use tui::{Frame, Terminal};
#[derive(Options)]
struct Opts {
#[options(help = "Help message")]
help: bool,
#[options(help = "Config file location")]
config: Option<PathBuf>,
}
struct App<'a> {
config: amdgpu_config::fan::Config,
events: Vec<(&'a str, &'a str)>,
table_state: TableState,
selected_point: Option<usize>,
}
impl<'a> App<'a> {
pub fn new(config: amdgpu_config::fan::Config) -> Self {
let mut table_state = TableState::default();
table_state.select(Some(0));
Self {
config,
events: vec![],
table_state,
selected_point: None,
}
}
/// Rotate through the event list.
/// This only exists to simulate some kind of "progress"
fn on_tick(&mut self) {
if self.events.is_empty() {
return;
}
let event = self.events.remove(0);
self.events.push(event);
}
}
fn main() -> Result<(), io::Error> {
let opts: Opts = Options::parse_args_default_or_exit();
let config_path = opts
.config
.as_deref()
.and_then(|p| p.to_str())
.unwrap_or(DEFAULT_FAN_CONFIG_PATH);
let config = amdgpu_config::fan::load_config(config_path).unwrap();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let tick_rate = Duration::from_millis(250);
let app = App::new(config);
run_app(&mut terminal, app, tick_rate).unwrap();
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Status::Exit = handle_event(&mut app)? {
return Ok(());
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
enum Status {
Continue,
Exit,
}
fn handle_event(app: &mut App) -> io::Result<Status> {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => return Ok(Status::Exit),
KeyCode::Char('w') => {
let s = toml::to_string_pretty(&app.config).unwrap();
std::fs::write(app.config.path(), &s)?;
}
KeyCode::Char(' ') => {
if let Some(index) = app.table_state.selected() {
app.selected_point = Some(index);
}
}
KeyCode::Down => match app.selected_point {
Some(index) => {
change_value(app, index, -1.0, y, y_mut);
}
None => match app.table_state.selected() {
Some(index) => {
if index + 1 < app.config.speed_matrix().len() {
app.table_state.select(Some(index + 1));
}
}
None => {
app.table_state.select(Some(0));
}
},
},
KeyCode::Up => match app.selected_point {
Some(index) => {
change_value(app, index, 1.0, y, y_mut);
}
None => {
if let Some(prev) = app
.table_state
.selected()
.and_then(|idx| idx.checked_sub(1))
{
app.table_state.select(Some(prev));
}
}
},
KeyCode::Left => {
if let Some(index) = app.selected_point {
change_value(app, index, -1.0, x, x_mut);
}
}
KeyCode::Right => {
if let Some(index) = app.selected_point {
change_value(app, index, 1.0, x, x_mut);
}
}
_ => {}
}
}
Ok(Status::Continue)
}
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let main_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(90), Constraint::Length(4)].as_ref())
.split(f.size());
{
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(30), Constraint::Min(100)].as_ref())
.split(main_chunks[0]);
{
let rows = app
.config
.speed_matrix()
.iter()
.enumerate()
.map(|(idx, point)| {
Row::new(vec![format!("{}", y(point)), format!("{}", x(point))]).style(
Style::default().fg(if Some(idx) == app.selected_point {
Color::Yellow
} else {
Color::White
}),
)
});
let table = Table::new(rows)
.block(Block::default())
.header(
Row::new(vec!["Speed", "Temp"])
.style(Style::default().fg(Color::Yellow))
.bottom_margin(1),
)
.widths(&[Constraint::Length(5), Constraint::Length(5)])
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
.highlight_symbol("*")
.column_spacing(1);
f.render_stateful_widget(table, chunks[0], &mut app.table_state);
}
{
let points = app
.config
.speed_matrix()
.iter()
.map(|point| (x(point), y(point)))
.collect::<Vec<_>>();
let dataset = vec![Dataset::default()
.data(&points)
.name("Temp/Speed")
.graph_type(GraphType::Scatter)
.marker(Marker::Dot)
.style(Style::default().fg(Color::White))];
let chart = Chart::new(dataset)
.block(
Block::default()
.style(Style::default().fg(Color::White))
.border_style(Style::default().fg(Color::White))
.borders(Borders::all()),
)
.style(Style::default())
.x_axis(Axis::default().title("Speed").bounds([0.0, 100.0]))
.y_axis(Axis::default().title("Temp").bounds([0.0, 100.0]));
f.render_widget(chart, chunks[1]);
}
{
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
]
.as_ref(),
)
.split(main_chunks[1]);
f.render_widget(Paragraph::new("q to exit"), chunks[0]);
f.render_widget(Paragraph::new("Move up/down to choose setting"), chunks[1]);
f.render_widget(
Paragraph::new("Press SPACE to select and up/down/left/right to change values"),
chunks[2],
);
f.render_widget(Paragraph::new("w to save"), chunks[3]);
}
}
}
fn change_value<Read, Write>(app: &mut App, index: usize, change: f64, read: Read, write: Write)
where
Read: FnOnce(&MatrixPoint) -> f64 + Copy,
Write: FnOnce(&mut MatrixPoint) -> &mut f64,
{
let prev = index
.checked_sub(1)
.and_then(|i| app.config.speed_matrix().get(i))
.map(read)
.unwrap_or(0.0);
let next = app
.config
.speed_matrix()
.get(index + 1)
.map(read)
.unwrap_or(100.0);
let current = app
.config
.speed_matrix()
.get(index)
.map(read)
.unwrap_or_default();
if (prev..=next).contains(&(current + change)) {
*app.config
.speed_matrix_mut()
.get_mut(index)
.map(write)
.unwrap() += change;
}
}
fn y_mut(p: &mut MatrixPoint) -> &mut f64 {
&mut p.speed
}
fn x_mut(p: &mut MatrixPoint) -> &mut f64 {
&mut p.temp
}
fn y(p: &MatrixPoint) -> f64 {
p.speed
}
fn x(p: &MatrixPoint) -> f64 {
p.temp
}

View File

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

View File

@ -1,27 +0,0 @@
[package]
name = "amdfand"
version = "1.0.13"
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.11", features = ["gui-helper"] }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
ron = { version = "0.1" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }

View File

@ -1,66 +0,0 @@
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
# AMDGPU Fan control service
Available commands:
* `service` - Set fan speed depends on GPU temperature
* `set-automatic` - Switch to GPU automatic fan speed control
* `set-manual` - Switch to GPU manual fan speed control
* `available` - Print available cards
#### amdfand set-automatic | set-manual [OPTIONS]
Optional arguments:
* -h, --help Help message
* -c, --card CARD GPU Card number
## Usage
```bash
cargo install amdfand
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
```toml
# /etc/amdfand/config.toml
log_level = "Error"
temp_input = "temp1_input"
[[speed_matrix]]
temp = 4.0
speed = 4.0
[[speed_matrix]]
temp = 30.0
speed = 33.0
[[speed_matrix]]
temp = 45.0
speed = 50.0
[[speed_matrix]]
temp = 60.0
speed = 66.0
[[speed_matrix]]
temp = 65.0
speed = 69.0
[[speed_matrix]]
temp = 70.0
speed = 75.0
[[speed_matrix]]
temp = 75.0
speed = 89.0
[[speed_matrix]]
temp = 80.0
speed = 100.0
```

View File

@ -1,180 +0,0 @@
use amdgpu::hw_mon::HwMon;
use amdgpu::utils::{linear_map, load_temp_inputs};
use amdgpu::{
utils, TempInput, PULSE_WIDTH_MODULATION_MANUAL, PULSE_WIDTH_MODULATION_MAX,
PULSE_WIDTH_MODULATION_MIN, PULSE_WIDTH_MODULATION_MODE,
};
use amdgpu_config::fan::Config;
use gumdrop::Options;
use crate::{change_mode, service};
#[derive(Debug, Options)]
pub struct AvailableCards {
#[options(help = "Help message")]
help: bool,
}
#[derive(Debug, Options)]
pub enum FanCommand {
#[options(help = "Check AMD GPU temperature and change fan speed depends on configuration")]
Service(service::Service),
#[options(help = "Switch GPU to automatic fan speed control")]
SetAutomatic(change_mode::Switcher),
#[options(help = "Switch GPU to manual fan speed control")]
SetManual(change_mode::Switcher),
#[options(help = "Print available cards")]
Available(AvailableCards),
}
#[derive(Debug, thiserror::Error)]
pub enum FanError {
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
NonIntPwm(std::num::ParseIntError),
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
NonIntTemp(std::num::ParseIntError),
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
EmptyTempSet,
#[error("Unable to change fan speed to manual mode. {0}")]
ManualSpeedFailed(utils::AmdGpuError),
#[error("Unable to change fan speed to automatic mode. {0}")]
AutomaticSpeedFailed(utils::AmdGpuError),
#[error("Unable to change AMD GPU modulation (a.k.a. speed) to {value}. {error}")]
FailedToChangeSpeed {
value: u64,
error: utils::AmdGpuError,
},
}
pub struct Fan {
pub hw_mon: HwMon,
/// List of available temperature inputs for current HW MOD
pub temp_inputs: Vec<String>,
/// Preferred temperature input
pub temp_input: Option<TempInput>,
/// 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 Fan {
type Target = HwMon;
fn deref(&self) -> &Self::Target {
&self.hw_mon
}
}
impl std::ops::DerefMut for Fan {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.hw_mon
}
}
const MODULATION_ENABLED_FILE: &str = "pwm1_enable";
impl Fan {
pub fn wrap(hw_mon: HwMon, config: &Config) -> Self {
Self {
temp_input: config.temp_input().copied(),
temp_inputs: load_temp_inputs(&hw_mon),
hw_mon,
pwm_min: None,
pwm_max: None,
}
}
pub fn wrap_all(v: Vec<HwMon>, config: &Config) -> Vec<Fan> {
v.into_iter().map(|hw| Self::wrap(hw, config)).collect()
}
/// 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;
self.write_pwm(pwm)?;
Ok(())
}
/// 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(())
}
/// 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_manual() {
self.write_manual()?;
}
self.hw_mon_write("pwm1", value)
.map_err(|error| FanError::FailedToChangeSpeed { value, error })?;
Ok(())
}
/// Check if gpu fan is managed by GPU embedded manager
pub fn is_fan_manual(&self) -> bool {
self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE)
.map(|s| s.as_str() == PULSE_WIDTH_MODULATION_MANUAL)
.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())?;
return Ok(value as f64 / 1000f64);
}
let mut results = Vec::with_capacity(self.temp_inputs.len());
for name in self.temp_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(FanError::EmptyTempSet)?;
Ok(value)
}
/// 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>()
.map_err(FanError::NonIntTemp)?;
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));
};
self.pwm_min.unwrap_or(0)
}
/// Read maximal 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));
};
self.pwm_max.unwrap_or(255)
}
}

View File

@ -1,24 +0,0 @@
use amdgpu::{utils, AmdGpuError};
use amdgpu_config::fan::ConfigError;
use crate::command::FanError;
#[derive(Debug, thiserror::Error)]
pub enum AmdFanError {
#[error("Vendor is not AMD")]
NotAmdCard,
#[error("No hwmod has been found in sysfs")]
NoHwMonFound,
#[error("No AMD Card has been found in sysfs")]
NoAmdCardFound,
#[error("{0}")]
AmdGpu(#[from] AmdGpuError),
#[error("{0}")]
Fan(#[from] FanError),
#[error("{0}")]
Config(#[from] ConfigError),
#[error("{0:}")]
Io(#[from] std::io::Error),
#[error("{0:}")]
AmdUtils(#[from] utils::AmdGpuError),
}

View File

@ -1,121 +0,0 @@
extern crate log;
use amdgpu::lock_file::PidLock;
use amdgpu::utils::{ensure_config_dir, hw_mons};
use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH};
use gumdrop::Options;
use crate::command::FanCommand;
use crate::error::AmdFanError;
mod change_mode;
mod command;
mod error;
mod panic_handler;
mod service;
pub type Result<T> = std::result::Result<T, AmdFanError>;
pub enum FanMode {
Manual,
Automatic,
}
#[derive(Options)]
pub struct Opts {
#[options(help = "Help message")]
help: bool,
#[options(help = "Print version")]
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();
if opts.version {
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
#[allow(deprecated)]
if config.cards().is_some() {
log::warn!("cards config field is no longer supported");
}
match opts.command {
None => run_service(config, opts),
Some(FanCommand::Service(_)) => run_service(config, opts),
Some(FanCommand::SetAutomatic(switcher)) => {
change_mode::run(switcher, FanMode::Automatic, config)
}
Some(FanCommand::SetManual(switcher)) => {
change_mode::run(switcher, FanMode::Manual, config)
}
Some(FanCommand::Available(_)) => {
println!("Available cards");
hw_mons(false)?.into_iter().for_each(|hw_mon| {
println!(
" * {:6>} - {}",
hw_mon.card(),
hw_mon.name().unwrap_or_default()
);
});
Ok(())
}
}
}
fn run_service(config: Config, opts: Opts) -> Result<()> {
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
}
fn setup() -> Result<(String, Config)> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "DEBUG");
}
pretty_env_logger::init();
ensure_config_dir()?;
let config_path = Opts::parse_args_default_or_exit()
.config
.unwrap_or_else(|| DEFAULT_FAN_CONFIG_PATH.to_string());
let config = load_config(&config_path)?;
log::info!("{:?}", config);
log::set_max_level(config.log_level().as_str().parse().unwrap());
Ok((config_path, config))
}
fn main() -> Result<()> {
let (_config_path, config) = match setup() {
Ok(config) => config,
Err(e) => {
log::error!("{}", e);
std::process::exit(1);
}
};
match run(config) {
Ok(()) => Ok(()),
Err(e) => {
panic_handler::restore_automatic();
log::error!("{}", e);
std::process::exit(1);
}
}
}

View File

@ -1,19 +0,0 @@
use amdgpu::utils::hw_mons;
use crate::command::Fan;
pub fn restore_automatic() {
for hw in hw_mons(true).unwrap_or_default() {
if let Err(error) = (Fan {
hw_mon: hw,
temp_inputs: vec![],
temp_input: None,
pwm_min: None,
pwm_max: None,
})
.write_automatic()
{
log::error!("{}", error);
}
}
}

View File

@ -1,72 +0,0 @@
use amdgpu::utils::hw_mons;
use amdgpu::{config_reloaded, is_reload_required, listen_unix_signal};
use amdgpu_config::fan::Config;
use gumdrop::Options;
use crate::command::Fan;
use crate::AmdFanError;
/// Start service which will change fan speed according to config and GPU
/// temperature
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() {
return Err(AmdFanError::NoHwMonFound);
}
hw_mons.iter().for_each(|fan| {
if let Err(e) = fan.write_manual() {
log::debug!(
"Failed to switch to manual fan manipulation for fan {:?}. {:?}",
fan.hw_mon,
e
);
}
});
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()
.and_then(|input| {
hw_mon
.read_gpu_temp(&input.as_string())
.map(|temp| temp as f64 / 1000f64)
.ok()
})
.or_else(|| hw_mon.max_gpu_temp().ok())
.unwrap_or_default();
log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp);
let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64);
if (last - gpu_temp).abs() < 0.001f64 {
log::debug!("Temperature didn't change");
continue;
};
let speed = config.speed_for_temp(gpu_temp);
log::debug!("Resolved speed {:.2}", speed);
if let Err(e) = hw_mon.set_speed(speed) {
log::error!("Failed to change speed to {}. {:?}", speed, e);
}
cache.insert(**hw_mon.card(), gpu_temp);
}
std::thread::sleep(std::time::Duration::from_secs(4));
}
}
#[derive(Debug, Options)]
pub struct Service {
#[options(help = "Help message")]
help: bool,
}

View File

@ -1,35 +0,0 @@
[package]
name = "amdgpu-config"
version = "1.0.10"
edition = "2021"
description = "Subcomponent of AMDGPU tools"
license = "MIT OR Apache-2.0"
keywords = ["hardware", "amdgpu"]
categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[lib]
name = "amdgpu_config"
path = "./src/lib.rs"
[features]
fan = []
voltage = []
monitor = []
gui = []
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
csv = { version = "1.1" }
thiserror = "1.0"
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] }

View File

@ -1,5 +0,0 @@
# amdgpu-config
This crates holds config files for `amdfand`, `amdvold` and `amdmond`.
For more information please check those services.

View File

@ -1,373 +0,0 @@
use amdgpu::utils::{ensure_config, linear_map};
use amdgpu::{LogLevel, TempInput};
pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml";
#[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 {
#[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 {
#[deprecated(
since = "1.0.6",
note = "Multi-card used is halted until we will have PC with multiple AMD GPU"
)]
pub fn cards(&self) -> Option<&Vec<String>> {
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),
_ => None,
}
}
pub fn speed_for_temp(&self, temp: f64) -> f64 {
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
Some(idx) => idx,
_ => return self.min_speed(),
};
if idx == self.speed_matrix.len() - 1 {
return self.max_speed();
}
linear_map(
temp,
self.speed_matrix[idx].temp,
self.speed_matrix[idx + 1].temp,
self.speed_matrix[idx].speed,
self.speed_matrix[idx + 1].speed,
)
}
pub fn log_level(&self) -> LogLevel {
self.log_level
}
pub fn temp_input(&self) -> Option<&TempInput> {
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)
}
fn max_speed(&self) -> f64 {
self.speed_matrix.last().map(|p| p.speed).unwrap_or(100f64)
}
}
impl Default for Config {
fn default() -> Self {
Self {
path: String::from(DEFAULT_FAN_CONFIG_PATH),
#[allow(deprecated)]
cards: None,
log_level: LogLevel::Error,
speed_matrix: vec![
MatrixPoint {
temp: 4f64,
speed: 4f64,
},
MatrixPoint {
temp: 30f64,
speed: 33f64,
},
MatrixPoint {
temp: 45f64,
speed: 50f64,
},
MatrixPoint {
temp: 60f64,
speed: 66f64,
},
MatrixPoint {
temp: 65f64,
speed: 69f64,
},
MatrixPoint {
temp: 70f64,
speed: 75f64,
},
MatrixPoint {
temp: 75f64,
speed: 89f64,
},
MatrixPoint {
temp: 80f64,
speed: 100f64,
},
],
temp_input: Some(TempInput(1)),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")]
FanSpeedTooLow { value: f64, index: usize },
#[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")]
FanSpeedTooHigh { value: f64, index: usize },
#[error(
"Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)]
UnsortedFanSpeed {
current: f64,
index: usize,
last: f64,
},
#[error(
"Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
)]
UnsortedFanTemp {
current: f64,
index: usize,
last: f64,
},
#[error("{0}")]
Io(#[from] std::io::Error),
}
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let mut config = ensure_config::<Config, ConfigError, _>(config_path)?;
config.path = String::from(config_path);
let mut last_point: Option<&MatrixPoint> = None;
for (index, matrix_point) in config.speed_matrix.iter().enumerate() {
if matrix_point.speed < 0f64 {
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed);
return Err(ConfigError::FanSpeedTooLow {
value: matrix_point.speed,
index,
});
}
if matrix_point.speed > 100f64 {
log::error!(
"Fan speed can't be above 100.0 found {}",
matrix_point.speed
);
return Err(ConfigError::FanSpeedTooHigh {
value: matrix_point.speed,
index,
});
}
if let Some(last_point) = last_point {
if matrix_point.speed < last_point.speed {
log::error!(
"Curve fan speeds should be monotonically increasing, found {} then {}",
last_point.speed,
matrix_point.speed
);
return Err(ConfigError::UnsortedFanSpeed {
current: matrix_point.speed,
last: last_point.speed,
index,
});
}
if matrix_point.temp < last_point.temp {
log::error!(
"Curve fan temps should be monotonically increasing, found {} then {}",
last_point.temp,
matrix_point.temp
);
return Err(ConfigError::UnsortedFanTemp {
current: matrix_point.temp,
last: last_point.temp,
index,
});
}
}
last_point = Some(matrix_point)
}
Ok(config)
}
#[cfg(test)]
mod parse_config {
use amdgpu::{AmdGpuError, Card, TempInput};
use serde::Deserialize;
#[derive(Deserialize, PartialEq, Eq, Debug)]
pub struct Foo {
card: Card,
}
#[test]
fn parse_card0() {
assert_eq!("card0".parse::<Card>(), Ok(Card(0)))
}
#[test]
fn parse_card1() {
assert_eq!("card1".parse::<Card>(), Ok(Card(1)))
}
#[test]
fn toml_card0() {
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
}
#[test]
fn parse_invalid_temp_input() {
assert_eq!(
"".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("".to_string()))
);
assert_eq!(
"12".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("12".to_string()))
);
assert_eq!(
"temp12".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("temp12".to_string()))
);
assert_eq!(
"12_input".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("12_input".to_string()))
);
assert_eq!(
"temp_12_input".parse::<TempInput>(),
Err(AmdGpuError::InvalidTempInput("temp_12_input".to_string()))
);
}
#[test]
fn parse_valid_temp_input() {
assert_eq!("temp12_input".parse::<TempInput>(), Ok(TempInput(12)));
}
}
#[cfg(test)]
mod speed_for_temp {
use super::*;
#[test]
fn below_minimal() {
let config = Config::default();
assert_eq!(config.speed_for_temp(1f64), 4f64);
}
#[test]
fn minimal() {
let config = Config::default();
assert_eq!(config.speed_for_temp(4f64), 4f64);
}
#[test]
fn between_3_and_4_temp_46() {
let config = Config::default();
// 45 -> 50
// 60 -> 66
assert_eq!(config.speed_for_temp(46f64).round(), 51f64);
}
#[test]
fn between_3_and_4_temp_58() {
let config = Config::default();
// 45 -> 50
// 60 -> 66
assert_eq!(config.speed_for_temp(58f64).round(), 64f64);
}
#[test]
fn between_3_and_4_temp_59() {
let config = Config::default();
// 45 -> 50
// 60 -> 66
assert_eq!(config.speed_for_temp(59f64).round(), 65f64);
}
#[test]
fn average() {
let config = Config::default();
assert_eq!(config.speed_for_temp(60f64), 66f64);
}
#[test]
fn max() {
let config = Config::default();
assert_eq!(config.speed_for_temp(80f64), 100f64);
}
#[test]
fn above_max() {
let config = Config::default();
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());
}
}

View File

@ -1,51 +0,0 @@
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,11 +0,0 @@
#[cfg(feature = "fan")]
pub mod fan;
#[cfg(feature = "gui")]
pub mod gui;
#[cfg(feature = "monitor")]
pub mod monitor;
#[cfg(feature = "voltage")]
pub mod voltage;
/// pulse width modulation fan level (0-255)
pub static PULSE_WIDTH_MODULATION: &str = "pwm1";

View File

@ -1,69 +0,0 @@
use amdgpu::utils::ensure_config;
use amdgpu::LogLevel;
use serde::{Deserialize, Serialize};
pub static DEFAULT_MONITOR_CONFIG_PATH: &str = "/etc/amdfand/monitor.toml";
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
/// Minimal log level
log_level: LogLevel,
/// Time in milliseconds
interval: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("{0}")]
Io(#[from] std::io::Error),
}
impl Default for Config {
fn default() -> Self {
Self {
log_level: LogLevel::Error,
interval: 5000,
}
}
}
impl Config {
pub fn log_level(&self) -> LogLevel {
self.log_level
}
pub fn interval(&self) -> u32 {
self.interval
}
}
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let mut config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
if config.interval < 100 {
log::warn!(
"Minimal interval is 100ms, overriding {}ms",
config.interval
);
config.interval = 100;
}
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

@ -1,48 +0,0 @@
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 {
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> {
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

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

View File

@ -1,27 +0,0 @@
[package]
name = "amdgpu"
version = "1.0.11"
edition = "2018"
description = "Subcomponent of AMDGPU fan control service"
license = "MIT OR Apache-2.0"
keywords = ["hardware", "amdgpu"]
categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[features]
gui-helper = []
[dependencies]
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
ron = { version = "0.7" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
nix = { version = "0.24" }
pidlock = { version = "0.1" }

View File

@ -1,5 +0,0 @@
# amdgpu-config
This is shared data for `amdfand`, `amdvold` and `amdmond`.
For more information please check those services.

View File

@ -1,84 +0,0 @@
use serde::Deserialize;
use crate::AmdGpuError;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Card(pub u32);
impl std::fmt::Display for Card {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!("card{}", self.0))
}
}
impl std::str::FromStr for Card {
type Err = AmdGpuError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if !value.starts_with("card") {
return Err(AmdGpuError::CardInvalidPrefix);
}
if value.len() < 5 {
return Err(AmdGpuError::CardInputTooShort);
}
value[4..]
.parse::<u32>()
.map_err(|e| AmdGpuError::CardInvalidSuffix(format!("{:?}", e)))
.map(Card)
}
}
impl<'de> Deserialize<'de> for Card {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct CardVisitor;
impl<'de> Visitor<'de> for CardVisitor {
type Value = u32;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("must have format cardX")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value.parse::<Card>() {
Ok(card) => Ok(*card),
Err(AmdGpuError::CardInvalidPrefix) => {
Err(E::custom(format!("expect cardX but got {}", value)))
}
Err(AmdGpuError::CardInvalidSuffix(s)) => Err(E::custom(s)),
Err(AmdGpuError::CardInputTooShort) => Err(E::custom(format!(
"{:?} must have at least 5 characters",
value
))),
_ => unreachable!(),
}
}
}
deserializer.deserialize_str(CardVisitor).map(Card)
}
}
impl serde::Serialize for Card {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl std::ops::Deref for Card {
type Target = u32;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -1,90 +0,0 @@
use std::fmt::{Debug, Display, Formatter};
use pidlock::PidlockError;
use crate::lock_file::LockFileError;
#[cfg(feature = "gui-helper")]
use crate::pidfile::helper_cmd::GuiHelperError;
use crate::pidfile::ports::PortsError;
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`.")]
CardInvalidPrefix,
#[error("Card must start with `card` and ends with a number. The given name is too short.")]
CardInputTooShort,
#[error("Value after `card` is invalid {0:}")]
CardInvalidSuffix(String),
#[error("Invalid temperature input")]
InvalidTempInput(String),
#[error("Unable to read GPU vendor")]
FailedReadVendor,
#[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),
#[error("{0:?}")]
Ports(#[from] PortsError),
#[error("{0:?}")]
LockFile(#[from] LockFileError),
}
#[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 {
fn eq(&self, other: &Self) -> bool {
use AmdGpuError::*;
match (self, other) {
(CardInvalidPrefix, CardInvalidPrefix) => true,
(CardInputTooShort, CardInputTooShort) => true,
(CardInvalidSuffix(a), CardInvalidSuffix(b)) => a == b,
(InvalidTempInput(a), InvalidTempInput(b)) => a == b,
(FailedReadVendor, FailedReadVendor) => true,
(NoAmdHwMon, NoAmdHwMon) => true,
(Io(a), Io(b)) => a.io.kind() == b.io.kind(),
_ => false,
}
}
}

View File

@ -1,122 +0,0 @@
use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR};
#[derive(Debug)]
pub struct HwMonName(pub String);
impl std::ops::Deref for HwMonName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct HwMon {
/// HW MOD cord (ex. card0)
pub card: Card,
/// MW MOD name (ex. hwmod0)
pub name: HwMonName,
}
impl HwMon {
pub fn new(card: &Card, name: HwMonName) -> Self {
Self { card: *card, name }
}
#[inline]
pub fn card(&self) -> &Card {
&self.card
}
#[inline]
pub fn name(&self) -> utils::Result<String> {
self.hw_mon_read("name")
}
#[inline]
pub fn is_amd(&self) -> bool {
self.device_read("vendor")
.map_err(|_| AmdGpuError::FailedReadVendor)
.map(|vendor| vendor.trim() == "0x1002")
.unwrap_or_default()
}
#[inline]
pub fn name_is_amd(&self) -> bool {
self.name().ok().filter(|s| s.trim() == "amdgpu").is_some()
}
fn mon_file_path(&self, name: &str) -> std::path::PathBuf {
self.mon_dir().join(name)
}
pub fn device_dir(&self) -> std::path::PathBuf {
std::path::PathBuf::new()
.join(ROOT_DIR)
.join(self.card.to_string())
.join("device")
}
pub fn mon_dir(&self) -> std::path::PathBuf {
self.device_dir().join("hwmon").join(&*self.name)
}
#[inline]
pub fn value_or<R: std::str::FromStr>(&self, name: &str, fallback: R) -> R {
self.hw_mon_read(name)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(fallback)
}
pub fn hw_mon_read(&self, name: &str) -> utils::Result<String> {
utils::read_to_string(self.mon_file_path(name)).map(|s| String::from(s.trim()))
}
pub fn device_read(&self, name: &str) -> utils::Result<String> {
utils::read_to_string(self.device_dir().join(name)).map(|s| String::from(s.trim()))
}
pub fn hw_mon_write(&self, name: &str, value: u64) -> utils::Result<()> {
utils::write(self.mon_file_path(name), format!("{}", value))?;
Ok(())
}
pub fn device_write<C: AsRef<[u8]>>(&self, name: &str, value: C) -> utils::Result<()> {
utils::write(self.device_dir().join(name), value)?;
Ok(())
}
}
#[inline]
fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf {
std::path::PathBuf::new()
.join(ROOT_DIR)
.join(card.to_string())
.join("device")
.join("hwmon")
}
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).map_err(|io| IoFailure {
io,
path: read_path,
})?;
let name = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
entry
.file_name()
.as_os_str()
.to_str()
.filter(|name| name.starts_with("hwmon"))
.map(String::from)
.map(HwMonName)
})
.take(1)
.last()
.ok_or(AmdGpuError::NoAmdHwMon)?;
Ok(HwMon::new(&card, name))
}

View File

@ -1,99 +0,0 @@
pub use card::*;
pub use error::*;
use serde::{Deserialize, Serialize};
pub use temp_input::*;
mod card;
mod error;
#[cfg(feature = "gui-helper")]
pub mod hw_mon;
pub mod lock_file;
pub mod pidfile;
mod temp_input;
pub mod utils;
pub static CONFIG_DIR: &str = "/etc/amdfand";
pub static ROOT_DIR: &str = "/sys/class/drm";
pub static HW_MON_DIR: &str = "hwmon";
/// pulse width modulation fan control minimum level (0)
pub static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min";
/// pulse width modulation fan control maximum level (255)
pub static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max";
/// pulse width modulation fan level (0-255)
pub static PULSE_WIDTH_MODULATION: &str = "pwm1";
/// pulse width modulation fan control method (0: no fan speed control, 1:
/// manual fan speed control using pwm interface, 2: automatic fan speed
/// control)
pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable";
// static PULSE_WIDTH_MODULATION_DISABLED: &str = "0";
pub static PULSE_WIDTH_MODULATION_MANUAL: &str = "1";
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)]
pub enum LogLevel {
/// A level lower than all log levels.
Off,
/// Corresponds to the `Error` log level.
Error,
/// Corresponds to the `Warn` log level.
Warn,
/// Corresponds to the `Info` log level.
Info,
/// Corresponds to the `Debug` log level.
Debug,
/// Corresponds to the `Trace` log level.
Trace,
}
impl LogLevel {
pub fn as_str(&self) -> &str {
match self {
LogLevel::Off => "OFF",
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
LogLevel::Debug => "DEBUG",
LogLevel::Trace => "TRACE",
}
}
}

View File

@ -1,157 +0,0 @@
//! Create lock file and prevent running 2 identical services.
//! NOTE: For 2 amdfand services you may just give 2 different names
use std::path::Path;
use nix::libc;
use crate::pidfile::Pid;
use crate::IoFailure;
#[derive(Debug, thiserror::Error)]
pub enum LockFileError {
#[error("Failed to read {path}. {err:?}")]
Unreadable { err: std::io::Error, path: String },
#[error("Pid {pid:?} file system error. {err:?}")]
Io { err: std::io::Error, pid: Pid },
#[error("Pid {0:?} does not exists")]
NotExists(Pid),
#[error("Pid {pid:?} with name {name:?} already exists")]
Conflict { name: String, pid: Pid },
#[error("Can't parse Pid value. {0:?}")]
MalformedPidFile(#[from] std::num::ParseIntError),
}
pub enum State {
NotExists,
Pending(Pid),
Dead,
New(Pid),
}
pub struct PidLock {
name: String,
pid_path: String,
}
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()
};
log::debug!("Creating pid lock for path {:?}", pid_path);
Ok(Self { pid_path, name })
}
/// Create new lock file. File will be created if:
/// * pid file does not exists
/// * pid file exists but process is dead
/// * old pid and current pid have different names (lock file exists after
/// reboot and PID was taken by other process)
pub fn acquire(&mut self) -> Result<(), crate::error::AmdGpuError> {
log::debug!("PID LOCK acquiring {}", self.pid_path);
let pid = self.process_pid();
if let Some(old) = self.old_pid() {
let old = old?;
if !self.is_alive(old) {
log::debug!("Old pid {:?} is dead, overriding...", old.0);
self.enforce_pid_file(pid)?;
return Ok(());
}
match self.process_name(old) {
Err(LockFileError::NotExists(..)) => {
log::debug!(
"Old pid {:?} doesn't have process stat, overriding....",
old.0
);
self.enforce_pid_file(old)?;
}
Err(e) => {
log::debug!("Lock error {:?}", e);
return Err(e.into());
}
Ok(name) if name.ends_with(&format!("/{}", self.name)) => {
log::warn!("Conflicting {} and {} for process {}", old.0, pid.0, name);
return Err(LockFileError::Conflict { pid: old, name }.into());
}
Ok(name /* name isn't the same */) => {
log::debug!(
"Old process {:?} and current process {:?} have different names, overriding....",
name, self.name
);
self.enforce_pid_file(old)?;
}
}
} else {
log::debug!("No collision detected");
self.enforce_pid_file(pid)?;
}
Ok(())
}
/// Remove lock file
pub fn release(&mut self) -> Result<(), crate::error::AmdGpuError> {
if let Err(e) = std::fs::remove_file(&self.pid_path) {
log::error!("Failed to release pid file {}. {:?}", self.pid_path, e);
}
Ok(())
}
/// Read old pid value from file
fn old_pid(&self) -> Option<Result<Pid, LockFileError>> {
match std::fs::read_to_string(&self.pid_path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => Some(Err(LockFileError::Unreadable {
path: self.pid_path.clone(),
err: e,
})),
Ok(s) => match s.parse::<i32>() {
Err(e) => Some(Err(LockFileError::MalformedPidFile(e))),
Ok(pid) => Some(Ok(Pid(pid))),
},
}
}
/// Check if PID is alive
fn is_alive(&self, pid: Pid) -> bool {
unsafe {
let result = libc::kill(pid.0, 0);
result == 0
}
}
/// Get current process PID
fn process_pid(&self) -> Pid {
Pid(std::process::id() as i32)
}
/// Read target process name
fn process_name(&self, pid: Pid) -> Result<String, LockFileError> {
match std::fs::read_to_string(format!("/proc/{}/cmdline", *pid)) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(LockFileError::NotExists(pid))
}
Err(err) => Err(LockFileError::Io { err, pid }),
Ok(s) => Ok(String::from(s.split('\0').next().unwrap_or_default())),
}
}
/// Override pid lock file
fn enforce_pid_file(&self, pid: Pid) -> Result<(), LockFileError> {
std::fs::write(&self.pid_path, format!("{}", pid.0))
.map_err(|e| LockFileError::Io { pid, err: e })
}
}

View File

@ -1,63 +0,0 @@
//! AMD GUI helper communication toolkit
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use crate::pidfile::{Pid, PidResponse};
#[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, 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),
}
impl PidResponse for Response {
fn kill_response() -> Self {
Self::NoOp
}
}
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,133 +0,0 @@
use std::fmt::Debug;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::net::Shutdown;
use std::ops::Deref;
use std::os::unix::net::UnixStream;
use serde::de::DeserializeOwned;
use serde::Serialize;
pub mod helper_cmd;
pub mod ports;
#[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
}
}
pub fn handle_connection<HandleCmd, Cmd, Res>(stream: UnixStream, handle_command: HandleCmd)
where
HandleCmd: FnOnce(Service<Res>, Cmd) + Copy,
Cmd: DeserializeOwned + Serialize + Debug,
Res: DeserializeOwned + Serialize + Debug + PidResponse,
{
let mut service = Service::<Res>::new(stream);
let command = match service.read_command() {
Some(s) => s,
_ => return service.kill(),
};
log::info!("Incoming {:?}", command);
let cmd = match ron::from_str::<Cmd>(command.trim()) {
Ok(cmd) => cmd,
Err(e) => {
log::warn!("Invalid message {:?}. {:?}", command, e);
return service.kill();
}
};
handle_command(service, cmd);
}
pub trait PidResponse: Sized {
fn kill_response() -> Self;
}
pub struct Service<Response>(UnixStream, PhantomData<Response>)
where
Response: serde::Serialize + Debug + PidResponse;
impl<Response> Service<Response>
where
Response: serde::Serialize + Debug + PidResponse,
{
pub fn new(file: UnixStream) -> Self {
Self(file, Default::default())
}
/// Serialize and send command
pub fn write_response(&mut self, res: Response) {
write_response(&mut self.0, res)
}
/// Read from `.sock` file new line separated commands
pub fn read_command(&mut self) -> Option<String> {
read_command(&mut self.0)
}
/// Close connection with no operation response
pub fn kill(mut self) {
self.write_response(Response::kill_response());
self.close();
}
pub fn close(self) {
let _ = self.0.shutdown(Shutdown::Both);
}
}
/// Serialize and send command
pub fn write_response<Response>(file: &mut UnixStream, res: Response)
where
Response: serde::Serialize + Debug,
{
match ron::to_string(&res) {
Ok(buffer) => match file.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(file: &mut UnixStream) -> Option<String> {
let mut command = String::with_capacity(100);
log::info!("Reading stream...");
read_line(file, &mut command);
if command.is_empty() {
return None;
}
Some(command)
}
pub 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;
}
}
}
}

View File

@ -1,273 +0,0 @@
//! AMD GUI helper communication toolkit
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::pidfile::PidResponse;
#[derive(Debug, thiserror::Error)]
pub enum PortsError {
#[error("AMD GPU ports socket file not found. Is service running?")]
NoSockFile,
#[error("Failed to connect to /tmp/amdgpu-ports.sock. {0}")]
UnableToConnect(#[from] std::io::Error),
#[error("Failed to ports command. {0}")]
Serialize(#[from] ron::Error),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum OutputType {
Reserved,
#[serde(rename = "v")]
Vga,
#[serde(rename = "m")]
MiniDvi,
#[serde(rename = "h")]
Hdmi,
#[serde(rename = "a")]
Audio,
#[serde(rename = "o")]
OpticalAudio,
#[serde(rename = "d")]
Dvi,
#[serde(rename = "t")]
Thunderbolt,
#[serde(rename = "D")]
DisplayPort,
#[serde(rename = "M")]
MiniDisplayPort,
#[serde(rename = "f")]
FireWire400,
#[serde(rename = "p")]
Ps2,
#[serde(rename = "s")]
Sata,
#[serde(rename = "e")]
ESata,
#[serde(rename = "E")]
Ethernet,
#[serde(rename = "F")]
FireWire800,
#[serde(rename = "1")]
UsbTypeA,
#[serde(rename = "2")]
UsbTypeB,
#[serde(rename = "3")]
UsbTypeC,
#[serde(rename = "4")]
MicroUsb,
#[serde(rename = "5")]
MimiUsb,
}
impl OutputType {
pub fn to_coords(&self) -> (u32, u32) {
match self {
OutputType::Reserved => (0, 0),
//
OutputType::Vga => (0, 0),
OutputType::MiniDvi => (80, 0),
OutputType::Hdmi => (160, 0),
OutputType::Audio => (240, 0),
OutputType::OpticalAudio => (320, 0),
//
OutputType::Dvi => (0, 80),
OutputType::Thunderbolt => (80, 80),
OutputType::DisplayPort => (160, 80),
OutputType::MiniDisplayPort => (240, 80),
OutputType::FireWire400 => (320, 80),
//
OutputType::Ps2 => (0, 160),
OutputType::Sata => (80, 160),
OutputType::ESata => (160, 160),
OutputType::Ethernet => (240, 160),
OutputType::FireWire800 => (320, 160),
//
OutputType::UsbTypeA => (0, 240),
OutputType::UsbTypeB => (80, 240),
OutputType::UsbTypeC => (160, 240),
OutputType::MicroUsb => (240, 240),
OutputType::MimiUsb => (320, 240),
}
}
pub fn name(&self) -> &str {
match self {
OutputType::Reserved => "-----",
//
OutputType::Vga => "Vga",
OutputType::MiniDvi => "MiniDvi",
OutputType::Hdmi => "Hdmi",
OutputType::Audio => "Audio",
OutputType::OpticalAudio => "OptimalAudio",
//
OutputType::Dvi => "Dvi",
OutputType::Thunderbolt => "Thunderbolt",
OutputType::DisplayPort => "DisplayPort",
OutputType::MiniDisplayPort => "MiniDisplayPort",
OutputType::FireWire400 => "FireWire400",
//
OutputType::Ps2 => "Ps2",
OutputType::Sata => "Sata",
OutputType::ESata => "ESata",
OutputType::Ethernet => "Ethernet",
OutputType::FireWire800 => "FireWire800",
//
OutputType::UsbTypeA => "UsbTypeA",
OutputType::UsbTypeB => "UsbTypeB",
OutputType::UsbTypeC => "UsbTypeC",
OutputType::MicroUsb => "MicroUsb",
OutputType::MimiUsb => "MimiUsb",
}
}
pub fn parse_str(s: &str) -> Option<Self> {
Some(match s {
"DP" => Self::DisplayPort,
"eDP" => Self::MiniDisplayPort,
"DVI" => Self::Dvi,
"HDMI" => Self::Hdmi,
_ => return None,
})
}
pub fn all() -> [OutputType; 20] {
[
OutputType::Vga,
OutputType::MiniDvi,
OutputType::Hdmi,
OutputType::Audio,
OutputType::OpticalAudio,
OutputType::Dvi,
OutputType::Thunderbolt,
OutputType::DisplayPort,
OutputType::MiniDisplayPort,
OutputType::FireWire400,
OutputType::Ps2,
OutputType::Sata,
OutputType::ESata,
OutputType::Ethernet,
OutputType::FireWire800,
OutputType::UsbTypeA,
OutputType::UsbTypeB,
OutputType::UsbTypeC,
OutputType::MicroUsb,
OutputType::MimiUsb,
]
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Output {
#[serde(rename = "c")]
pub card: String,
#[serde(rename = "t")]
pub port_type: String,
#[serde(rename = "T")]
pub ty: Option<OutputType>,
#[serde(rename = "m")]
pub port_name: Option<String>,
#[serde(rename = "n")]
pub port_number: u8,
#[serde(rename = "s")]
pub status: Status,
#[serde(rename = "M")]
pub modes: Vec<OutputMode>,
pub display_power_managment: bool,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct OutputMode {
#[serde(rename = "w")]
pub width: u16,
#[serde(rename = "h")]
pub height: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Status {
#[serde(rename = "c")]
Connected,
#[serde(rename = "d")]
Disconnected,
}
impl Default for Status {
fn default() -> Self {
Self::Disconnected
}
}
impl Output {
fn to_path(&self) -> PathBuf {
PathBuf::new().join("/sys/class/drm").join(format!(
"card{}-{}{}-{}",
self.card,
self.port_type,
self.port_name
.as_deref()
.map(|s| format!("-{s}"))
.unwrap_or_default(),
self.port_number
))
}
fn status_path(&self) -> PathBuf {
self.to_path().join("status")
}
pub fn read_status(&self) -> Option<Status> {
Some(
match std::fs::read_to_string(self.status_path()).ok()?.trim() {
"connected" => Status::Connected,
"disconnected" => Status::Disconnected,
_ => return None,
},
)
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Command {
Ports,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum Response {
Ports(Vec<Output>),
NoOp,
}
impl PidResponse for Response {
fn kill_response() -> Self {
Self::NoOp
}
}
pub fn sock_file() -> PathBuf {
std::path::Path::new("/tmp").join("amdgpu-ports.sock")
}
pub fn send_command(cmd: Command) -> crate::Result<Response> {
let sock_path = sock_file();
if !sock_path.exists() {
return Err(PortsError::NoSockFile.into());
}
let mut stream = UnixStream::connect(&sock_path).map_err(PortsError::UnableToConnect)?;
let s = ron::to_string(&cmd).map_err(PortsError::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(PortsError::Serialize)?
};
Ok(res)
}

View File

@ -1,79 +0,0 @@
use serde::Serializer;
use crate::AmdGpuError;
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct TempInput(pub u16);
impl TempInput {
pub fn as_string(&self) -> String {
format!("temp{}_input", self.0)
}
}
impl std::str::FromStr for TempInput {
type Err = AmdGpuError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.starts_with("temp") && s.ends_with("_input") {
let mut buffer = String::with_capacity(4);
for c in s[4..].chars() {
if c.is_numeric() {
buffer.push(c);
} else if buffer.is_empty() {
return Err(AmdGpuError::InvalidTempInput(s.to_string()));
}
}
buffer
.parse()
.map_err(|e| {
log::error!("Temp input error {:?}", e);
AmdGpuError::InvalidTempInput(s.to_string())
})
.map(Self)
} else {
Err(AmdGpuError::InvalidTempInput(s.to_string()))
}
}
}
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
D: serde::Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct TempInputVisitor;
impl<'de> Visitor<'de> for TempInputVisitor {
type Value = u16;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("must have format cardX")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
match value.parse::<TempInput>() {
Ok(temp) => Ok(temp.0),
_ => unreachable!(),
}
}
}
deserializer
.deserialize_str(TempInputVisitor)
.map(|v| TempInput(v as u16))
}
}

View File

@ -1,128 +0,0 @@
use std::io::ErrorKind;
use crate::hw_mon::HwMon;
use crate::{hw_mon, Card, CONFIG_DIR, ROOT_DIR};
pub type Result<T> = std::result::Result<T, AmdGpuError>;
#[derive(Debug, thiserror::Error)]
pub enum AmdGpuError {
#[error("Write to {path:?} failed. {io}")]
Write { io: std::io::Error, path: String },
#[error("Read from {path:?} failed. {io}")]
Read { io: std::io::Error, path: String },
#[error("File {0:?} does not exists")]
FileNotFound(String),
}
pub fn read_to_string<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
std::fs::read_to_string(&path).map_err(|io| {
if io.kind() == std::io::ErrorKind::NotFound {
AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default())
} else {
AmdGpuError::Read {
io,
path: path.as_ref().to_str().map(String::from).unwrap_or_default(),
}
}
})
}
pub fn write<P: AsRef<std::path::Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
std::fs::write(&path, contents).map_err(|io| {
if io.kind() == std::io::ErrorKind::NotFound {
AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default())
} else {
AmdGpuError::Write {
io,
path: path.as_ref().to_str().map(String::from).unwrap_or_default(),
}
}
})
}
/// linear mapping from the xrange to the yrange
pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 {
let m = (y2 - y1) / (x2 - x1);
m * (x - x1) + y1
}
/// Read all available graphic cards from direct rendering manager
pub fn read_cards() -> std::io::Result<Vec<Card>> {
Ok(std::fs::read_dir(ROOT_DIR)?
.into_iter()
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().as_os_str().to_str().map(String::from))
.filter_map(|file_name| file_name.parse::<Card>().ok())
.collect())
}
/// Wrap cards in HW Mon manipulator and
/// filter cards so only amd and listed in config cards are accessible
pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
Ok(read_cards()?
.into_iter()
.flat_map(|card| {
log::info!("opening hw mon for {:?}", card);
hw_mon::open_hw_mon(card)
})
.filter(|hw_mon| {
!filter || {
log::info!("is vendor ok? {}", hw_mon.is_amd());
hw_mon.is_amd()
}
})
.filter(|hw_mon| {
!filter || {
log::info!("is hwmon name ok? {}", hw_mon.name_is_amd());
hw_mon.name_is_amd()
}
})
.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,
P: AsRef<std::path::Path>,
Error: From<std::io::Error>,
{
match std::fs::read_to_string(&config_path) {
Ok(s) => Ok(toml::from_str::<Config>(s.as_str()).unwrap()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let config = Config::default();
std::fs::write(config_path, toml::to_string(&config).unwrap())?;
Ok(config)
}
Err(e) => {
log::error!("{:?}", e);
panic!();
}
}
}
/// 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,
_ => return vec![],
};
dir.filter_map(|f| f.ok())
.filter_map(|f| {
f.file_name()
.to_str()
.filter(|s| s.starts_with("temp") && s.ends_with("_input"))
.map(String::from)
})
.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)?;
}
Ok(())
}

View File

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

View File

@ -1,33 +0,0 @@
[package]
name = "amdgui-helper"
version = "1.0.10"
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", features = ["derive"] }
toml = { version = "0.5" }
ron = { version = "0.7" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
nix = { version = "0.23" }
sudo = { version = "0.6" }
[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" }

View File

@ -1,7 +0,0 @@
# 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`.

View File

@ -1,127 +0,0 @@
//! 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 std::ffi::OsStr;
use std::fs::Permissions;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixListener;
use amdgpu::pidfile::helper_cmd::{Command, Response};
use amdgpu::pidfile::{handle_connection, Pid};
use amdgpu::IoFailure;
#[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::pidfile::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(0o777)) {
log::error!("Failed to change gui helper socket file mode. {:?}", e);
}
while let Ok((stream, _addr)) = listener.accept() {
handle_connection::<_, Command, Response>(stream, handle_command);
}
lock.release()?;
Ok(())
}
pub type Service = amdgpu::pidfile::Service<Response>;
fn handle_command(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(service, path, content),
}
}
fn handle_save_fan_config(mut service: 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 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![]
}
}

View File

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

View File

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

View File

@ -1,73 +0,0 @@
[package]
name = "amdguid"
version = "1.0.12"
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 = ["glium", "egui_glium", "_gui"]
xorg-glow = ["glow", "egui_glow", "glutin", "_gui"]
default = ["wayland"]
_gui = [
"egui",
"epaint",
"epi",
"winit",
"egui-winit",
]
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan", "gui"] }
amdmond-lib = { path = "../amdmond-lib", version = "1.0.10" }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
tracing = { version = "0.1.36" }
tracing-subscriber = { version = "0.3.15" }
egui = { version = "0.18", optional = true, features = [] }
epaint = { version = "0.18", features = [], optional = true }
epi = { version = "0.17.0", optional = true }
winit = { version = "0.26", optional = true }
egui-winit = { version = "0.18", optional = true }
# vulkan
egui_vulkano = { version = "0.8.0", optional = true }
vulkano-win = { version = "0.29.0", optional = true }
vulkano = { version = "0.29.0", optional = true }
vulkano-shaders = { version = "0.29.0", optional = true }
bytemuck = { version = "*" }
# xorg glium
glium = { version = "0.32.1", optional = true }
egui_glium = { version = "0.18.0", optional = true }
# xorg glow
glutin = { version = "0.29", optional = true }
glow = { version = "0.11", optional = true }
egui_glow = { version = "0.18", optional = true }
tokio = { version = "1.15", features = ["full"] }
parking_lot = { version = "0.12" }
nix = { version = "0.25" }
image = { version = "0.24.2" }
emath = { version = "0.18" }
[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" }

View File

@ -1,15 +0,0 @@
# 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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,317 +0,0 @@
use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use amdgpu::pidfile::ports::{Output, OutputType};
use amdgpu::pidfile::Pid;
use egui::Ui;
use epaint::ColorImage;
use epi::Frame;
use image::{GenericImageView, ImageBuffer, ImageFormat};
use parking_lot::Mutex;
use crate::widgets::outputs_settings::OutputsSettings;
use crate::widgets::{ChangeFanSettings, CoolingPerformance};
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 std::ops::Deref for FanServices {
type Target = Vec<FanService>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
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,
Outputs,
Settings,
}
impl Default for Page {
fn default() -> Self {
Self::Config
}
}
pub type FanConfig = Arc<Mutex<amdgpu_config::fan::Config>>;
#[cfg(not(debug_assertions))]
static RELOAD_PID_LIST_DELAY: u8 = 18;
#[cfg(debug_assertions)]
static RELOAD_PID_LIST_DELAY: u8 = 80;
pub struct StatefulConfig {
pub config: FanConfig,
pub state: ChangeState,
pub textures: HashMap<OutputType, epaint::TextureHandle>,
}
impl StatefulConfig {
pub fn new(config: FanConfig) -> Self {
let textures = HashMap::with_capacity(40);
Self {
config,
state: ChangeState::New,
textures,
}
}
pub fn load_textures(&mut self, ui: &mut Ui) {
if !self.textures.is_empty() {
return;
}
// 80x80
let image = {
let bytes = include_bytes!("../assets/icons/ports2.png");
image::load_from_memory_with_format(bytes, ImageFormat::Png).unwrap()
};
let ctx = ui.ctx();
for ty in OutputType::all() {
let (offset_x, offset_y) = ty.to_coords();
let mut img = ImageBuffer::new(80, 80);
for x in 0..80 {
for y in 0..80 {
img.put_pixel(x, y, image.get_pixel(x + offset_x, y + offset_y));
}
}
let size = [img.width() as _, img.height() as _];
let pixels = img.as_flat_samples();
let id = ctx.load_texture(
String::from(ty.name()),
epaint::ImageData::Color(ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
)),
);
self.textures.insert(ty, id);
}
}
}
pub struct AmdGui {
pub page: Page,
pid_files: SocketState<FanServices>,
outputs: SocketState<BTreeMap<String, Vec<Output>>>,
cooling_performance: CoolingPerformance,
change_fan_settings: ChangeFanSettings,
outputs_settings: OutputsSettings,
config: StatefulConfig,
reload_pid_list_delay: u8,
}
impl epi::App for AmdGui {
fn update(&mut self, _ctx: &epi::egui::Context, _frame: &Frame) {}
fn name(&self) -> &str {
"AMD GUI"
}
}
impl AmdGui {
pub fn new_with_config(config: FanConfig) -> Self {
Self {
page: Default::default(),
pid_files: SocketState::NotAvailable,
outputs: SocketState::NotAvailable,
cooling_performance: CoolingPerformance::new(100, config.clone()),
change_fan_settings: ChangeFanSettings::new(config.clone()),
outputs_settings: OutputsSettings::default(),
config: StatefulConfig::new(config),
reload_pid_list_delay: 0,
}
}
pub fn ui(&mut self, ui: &mut Ui) {
self.config.load_textures(ui);
match self.page {
Page::Config => {
if let SocketState::Connected(pid_files) = &mut self.pid_files {
self.change_fan_settings
.draw(ui, pid_files, &mut self.config);
} else {
ui.label("Not available");
}
}
Page::Monitoring => {
if let SocketState::Connected(pid_files) = &mut self.pid_files {
self.cooling_performance.draw(ui, pid_files);
} else {
ui.label("Not available");
}
}
Page::Settings => {}
Page::Outputs => {
if let SocketState::Connected(outputs) = &self.outputs {
self.outputs_settings.draw(ui, &mut self.config, outputs);
} else {
ui.label("Not available");
}
}
}
}
pub fn tick(&mut self) {
self.cooling_performance.tick();
let can_decrease = self.reload_pid_list_delay > 0;
if can_decrease {
self.reload_pid_list_delay -= 1;
return;
}
self.reload_pid_list_delay = RELOAD_PID_LIST_DELAY;
{
use amdgpu::pidfile::helper_cmd::{send_command, Command, Response};
match send_command(Command::FanServices) {
Ok(Response::Services(services))
if self
.pid_files
.connected()
.map(|c| c.list_changed(&services))
.unwrap_or(true) =>
{
self.pid_files = SocketState::Connected(FanServices::from(services));
}
Ok(Response::Services(_services)) => {
// SKIP
}
Ok(res) => {
tracing::warn!("Unexpected response {:?} while loading fan services", res);
}
Err(e) => {
self.pid_files = SocketState::NotAvailable;
tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
}
}
}
{
use amdgpu::pidfile::ports::{send_command, Command, Response};
match send_command(Command::Ports) {
Ok(Response::NoOp) => {}
Ok(Response::Ports(outputs)) => {
let mut names = outputs.iter().fold(
Vec::with_capacity(outputs.len()),
|mut set, output| {
set.push(output.card.clone());
set
},
);
names.sort();
let mut tree = BTreeMap::new();
names.into_iter().for_each(|name| {
tree.insert(name, Vec::with_capacity(6));
});
self.outputs = SocketState::Connected(outputs.into_iter().fold(
tree,
|mut agg, output| {
let v = agg
.entry(output.card.clone())
.or_insert_with(|| Vec::with_capacity(6));
v.push(output);
v.sort_by(|a, b| {
format!(
"{}{}{}",
a.port_type,
a.port_name.as_deref().unwrap_or_default(),
a.port_number,
)
.cmp(&format!(
"{}{}{}",
b.port_type,
b.port_name.as_deref().unwrap_or_default(),
b.port_number,
))
});
agg
},
));
}
Err(e) => {
if matches!(self.page, Page::Outputs) {
self.page = Page::Config;
}
self.outputs = SocketState::NotAvailable;
tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
}
}
}
}
}
pub enum SocketState<Content> {
NotAvailable,
Connected(Content),
}
impl<C> SocketState<C> {
pub fn connected(&self) -> Option<&C> {
match self {
Self::Connected(c) => Some(c),
_ => None,
}
}
}

View File

@ -1,91 +0,0 @@
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use glium::glutin;
use image::RgbaImage;
use parking_lot::Mutex;
use tokio::sync::mpsc::UnboundedReceiver;
use crate::app::{AmdGui, ImageStorage, ImageType};
use crate::backend::create_ui;
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>>, mut receiver: UnboundedReceiver<bool>) {
let event_loop = glutin::event_loop::EventLoop::with_user_event();
let display = create_display(&event_loop);
let mut egui = egui_glium::EguiGlium::new(&display);
let proxy = event_loop.create_proxy();
tokio::spawn(async move {
loop {
if receiver.recv().await.is_some() {
if let Err(e) = proxy.send_event(()) {
tracing::error!("{:?}", e);
}
}
}
});
event_loop.run(move |event, _, control_flow| {
let mut redraw = || {
egui.begin_frame(&display);
create_ui(amd_gui.clone(), egui.ctx());
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::UserEvent(_) | 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();
}
_ => (),
}
});
}

View File

@ -1,122 +0,0 @@
use std::sync::Arc;
use parking_lot::Mutex;
use tokio::sync::mpsc::UnboundedReceiver;
use crate::backend::create_ui;
use crate::AmdGui;
fn create_display(
event_loop: &glutin::event_loop::EventLoop<()>,
) -> (
glutin::WindowedContext<glutin::PossiblyCurrent>,
::glow::Context,
) {
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 gl_window = unsafe {
glutin::ContextBuilder::new()
.with_depth_buffer(0)
.with_srgb(true)
.with_stencil_buffer(0)
.with_vsync(true)
.build_windowed(window_builder, event_loop)
.unwrap()
.make_current()
.unwrap()
};
let gl = unsafe { ::glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
unsafe {
use glow::HasContext as _;
gl.enable(glow::FRAMEBUFFER_SRGB);
}
(gl_window, gl)
}
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>, mut receiver: UnboundedReceiver<bool>) {
let event_loop = glutin::event_loop::EventLoop::with_user_event();
let (gl_window, gl) = create_display(&event_loop);
let mut egui = egui_glow::EguiGlow::new(&gl_window, &gl);
let proxy = event_loop.create_proxy();
tokio::spawn(async move {
loop {
if receiver.recv().await.is_some() {
if let Err(e) = proxy.send_event(()) {
tracing::error!("{:?}", e);
}
}
}
});
event_loop.run(move |event, _, control_flow| {
let mut redraw = || {
egui.begin_frame(gl_window.window());
create_ui(amd_gui.clone(), egui.ctx());
let (needs_repaint, shapes) = egui.end_frame(gl_window.window());
*control_flow = if needs_repaint {
gl_window.window().request_redraw();
glutin::event_loop::ControlFlow::Poll
} else {
glutin::event_loop::ControlFlow::Wait
};
{
let color = egui::Rgba::from_rgb(0.1, 0.3, 0.2);
unsafe {
use glow::HasContext as _;
gl.clear_color(color[0], color[1], color[2], color[3]);
gl.clear(glow::COLOR_BUFFER_BIT);
}
// draw things behind egui here
egui.paint(&gl_window, &gl, shapes);
// draw things on top of egui here
gl_window.swap_buffers().unwrap();
}
};
match event {
glutin::event::Event::UserEvent(_) | glutin::event::Event::RedrawRequested(_) => {
redraw()
}
glutin::event::Event::WindowEvent { event, .. } => {
if egui.is_quit_event(&event) {
*control_flow = glutin::event_loop::ControlFlow::Exit;
}
if let glutin::event::WindowEvent::Resized(physical_size) = event {
gl_window.resize(physical_size);
}
egui.on_event(&event);
gl_window.window().request_redraw(); // TODO: ask egui if the
// events warrants a
// repaint instead
}
glutin::event::Event::LoopDestroyed => {
egui.destroy(&gl);
}
_ => (),
}
});
}

View File

@ -1,79 +0,0 @@
#[cfg(feature = "xorg-glium")]
pub mod glium_backend;
#[cfg(feature = "xorg-glow")]
pub mod glow_backend;
#[cfg(feature = "wayland")]
pub mod wayland_backend;
use std::sync::Arc;
use egui::panel::TopBottomSide;
use egui::{Layout, PointerButton};
#[cfg(feature = "xorg-glium")]
pub use glium_backend::*;
#[cfg(feature = "xorg-glow")]
pub use glow_backend::*;
use parking_lot::Mutex;
#[cfg(feature = "wayland")]
pub use wayland_backend::*;
use crate::app::Page;
use crate::AmdGui;
pub fn create_ui(amd_gui: Arc<Mutex<AmdGui>>, ctx: &egui::Context) {
egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show(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("Outputs"), /* .text_style(TextStyle::Heading) */
)
.clicked_by(PointerButton::Primary)
{
amd_gui.lock().page = Page::Outputs;
}
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(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::Outputs => {
gui.ui(ui);
}
Page::Settings => {
ctx.settings_ui(ui);
}
}
});
}

View File

@ -1,447 +0,0 @@
use std::convert::{TryFrom, TryInto};
use std::sync::Arc;
use bytemuck::{Pod, Zeroable};
use egui_vulkano::UpdateTexturesResult;
use parking_lot::Mutex;
use tokio::sync::mpsc::UnboundedReceiver;
use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer, TypedBufferAccess};
use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, SubpassContents};
use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType};
use vulkano::device::{Device, DeviceCreateInfo, DeviceExtensions, QueueCreateInfo};
use vulkano::format::Format;
use vulkano::image::view::ImageView;
use vulkano::image::{ImageAccess, ImageUsage, SwapchainImage};
use vulkano::instance::{Instance, InstanceCreateInfo};
use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
use vulkano::pipeline::graphics::vertex_input::BuffersDefinition;
use vulkano::pipeline::graphics::viewport::{Viewport, ViewportState};
use vulkano::pipeline::GraphicsPipeline;
use vulkano::render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass};
use vulkano::swapchain::{AcquireError, Swapchain, SwapchainCreateInfo, SwapchainCreationError};
use vulkano::sync::{FenceSignalFuture, FlushError, GpuFuture};
use vulkano::{swapchain, sync};
use vulkano_win::VkSurfaceBuild;
use winit::event::{Event, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::{Window, WindowBuilder};
use crate::app::AmdGui;
use crate::backend::create_ui;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
struct Vertex {
position: [f32; 2],
color: [f32; 4],
}
pub enum FrameEndFuture<F: GpuFuture + 'static> {
FenceSignalFuture(FenceSignalFuture<F>),
BoxedFuture(Box<dyn GpuFuture>),
}
impl<F: GpuFuture> FrameEndFuture<F> {
pub fn now(device: Arc<Device>) -> Self {
Self::BoxedFuture(sync::now(device).boxed())
}
pub fn get(self) -> Box<dyn GpuFuture> {
match self {
FrameEndFuture::FenceSignalFuture(f) => f.boxed(),
FrameEndFuture::BoxedFuture(f) => f,
}
}
}
impl<F: GpuFuture> AsMut<dyn GpuFuture> for FrameEndFuture<F> {
fn as_mut(&mut self) -> &mut (dyn GpuFuture + 'static) {
match self {
FrameEndFuture::FenceSignalFuture(f) => f,
FrameEndFuture::BoxedFuture(f) => f,
}
}
}
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>, _receiver: UnboundedReceiver<bool>) {
let required_extensions = vulkano_win::required_extensions();
let instance = Instance::new(InstanceCreateInfo {
application_name: Some("amdguid".into()),
enabled_extensions: required_extensions,
..Default::default()
})
.unwrap();
let physical = {
let mut v = PhysicalDevice::enumerate(&instance).collect::<Vec<_>>();
v.sort_by(|a, b| a.api_version().cmp(&b.api_version()));
v.remove(0)
};
tracing::info!(
"Using device: {} (type: {:?})",
physical.properties().device_name,
physical.properties().device_type,
);
let event_loop = EventLoop::new();
let surface = WindowBuilder::new()
.with_title("AMD GUID")
// .with_fullscreen(Some(Fullscreen::Borderless(None)))
.build_vk_surface(&event_loop, instance.clone())
.unwrap();
let device_extensions = DeviceExtensions {
khr_swapchain: true,
..DeviceExtensions::none()
};
let (physical_device, queue_family) = PhysicalDevice::enumerate(&instance)
.filter(|&p| p.supported_extensions().is_superset_of(&device_extensions))
.filter_map(|p| {
p.queue_families()
.find(|&q| q.supports_graphics() && q.supports_surface(&surface).unwrap_or(false))
.map(|q| (p, q))
})
.min_by_key(|(p, _)| match p.properties().device_type {
PhysicalDeviceType::DiscreteGpu => 0,
PhysicalDeviceType::IntegratedGpu => 1,
PhysicalDeviceType::VirtualGpu => 2,
PhysicalDeviceType::Cpu => 3,
PhysicalDeviceType::Other => 4,
})
.unwrap();
let (device, mut queues) = Device::new(
physical_device,
DeviceCreateInfo {
enabled_extensions: physical_device
.required_extensions()
.union(&device_extensions),
queue_create_infos: vec![QueueCreateInfo::family(queue_family)],
..Default::default()
},
)
.unwrap();
let queue = queues.next().unwrap();
let (mut swapchain, images) = {
let caps = physical_device
.surface_capabilities(&surface, Default::default())
.unwrap();
let composite_alpha = caps.supported_composite_alpha.iter().next().unwrap();
let image_format = Some(Format::B8G8R8A8_SRGB);
let image_extent: [u32; 2] = surface.window().inner_size().into();
Swapchain::new(
device.clone(),
surface.clone(),
SwapchainCreateInfo {
min_image_count: caps.min_image_count,
image_format,
image_extent,
image_usage: ImageUsage::color_attachment(),
composite_alpha,
..Default::default()
},
)
.unwrap()
};
#[derive(Default, Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct Vertex {
position: [f32; 2],
}
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()
};
mod vs {
#![allow(clippy::needless_question_mark)]
vulkano_shaders::shader! {
ty: "vertex",
src: "
#version 450
layout(location = 0) in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
"
}
}
mod fs {
#![allow(clippy::needless_question_mark)]
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);
}
"
}
}
let vs = vs::load(device.clone()).unwrap();
let fs = fs::load(device.clone()).unwrap();
let render_pass = vulkano::ordered_passes_renderpass!(
device.clone(),
attachments: {
color: {
load: Clear,
store: Store,
format: swapchain.image_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 = GraphicsPipeline::start()
.vertex_input_state(BuffersDefinition::new().vertex::<Vertex>())
.vertex_shader(vs.entry_point("main").unwrap(), ())
.input_assembly_state(InputAssemblyState::new())
.viewport_state(ViewportState::viewport_dynamic_scissor_irrelevant())
.fragment_shader(fs.entry_point("main").unwrap(), ())
.render_pass(Subpass::from(render_pass.clone(), 0).unwrap())
.build(device.clone())
.unwrap();
let mut viewport = Viewport {
origin: [0.0, 0.0],
dimensions: [0.0, 0.0],
depth_range: 0.0..1.0,
};
let mut frame_buffers =
window_size_dependent_setup(&images, render_pass.clone(), &mut viewport);
let mut recreate_swapchain = false;
let mut previous_frame_end = Some(FrameEndFuture::now(device.clone()));
//Set up everything need to draw the gui
let window = surface.window();
let egui_ctx = egui::Context::default();
let mut egui_winit = egui_winit::State::new(4096, window);
let mut egui_painter = egui_vulkano::Painter::new(
device.clone(),
queue.clone(),
Subpass::from(render_pass.clone(), 1).unwrap(),
)
.unwrap();
//Set up some window to look at for the test
event_loop.run(move |event, _, control_flow| {
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
*control_flow = ControlFlow::Exit;
}
Event::WindowEvent {
event: WindowEvent::Resized(_),
..
} => {
recreate_swapchain = 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()
.as_mut()
.cleanup_finished();
if recreate_swapchain {
let dimensions: [u32; 2] = surface.window().inner_size().into();
let (new_swapchain, new_images) =
match swapchain.recreate(SwapchainCreateInfo {
image_extent: surface.window().inner_size().into(),
..swapchain.create_info()
}) {
Ok(r) => r,
Err(SwapchainCreationError::ImageExtentNotSupported { .. }) => return,
Err(e) => panic!("Failed to recreate swapchain: {:?}", e),
};
swapchain = new_swapchain;
frame_buffers = window_size_dependent_setup(
&new_images,
render_pass.clone(),
&mut viewport,
);
viewport.dimensions = [dimensions[0] as f32, dimensions[1] as f32];
recreate_swapchain = false;
}
let (image_num, suboptimal, acquire_future) =
match swapchain::acquire_next_image(swapchain.clone(), None) {
Ok(r) => r,
Err(AcquireError::OutOfDate) => {
recreate_swapchain = true;
return;
}
Err(e) => panic!("Failed to acquire next image: {:?}", e),
};
if suboptimal {
recreate_swapchain = 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();
egui_ctx.begin_frame(egui_winit.take_egui_input(surface.window()));
create_ui(amd_gui.clone(), &egui_ctx);
let egui_output = egui_ctx.end_frame();
let platform_output = egui_output.platform_output;
egui_winit.handle_platform_output(surface.window(), &egui_ctx, platform_output);
let result = egui_painter
.update_textures(egui_output.textures_delta, &mut builder)
.expect("egui texture error");
let wait_for_last_frame = result == UpdateTexturesResult::Changed;
// Do your usual rendering
builder
.begin_render_pass(
frame_buffers[image_num].clone(),
SubpassContents::Inline,
clear_values,
)
.unwrap()
.set_viewport(0, [viewport.clone()])
.bind_pipeline_graphics(pipeline.clone())
.bind_vertex_buffers(0, vertex_buffer.clone())
.draw(vertex_buffer.len().try_into().unwrap(), 1, 0, 0)
.unwrap(); // Don't end the render pass yet
// Build your gui
// Automatically start the next render subpass and draw the gui
let size = surface.window().inner_size();
let sf: f32 = surface.window().scale_factor() as f32;
egui_painter
.draw(
&mut builder,
[(size.width as f32) / sf, (size.height as f32) / sf],
&egui_ctx,
egui_output.shapes,
)
.unwrap();
// End the render pass as usual
builder.end_render_pass().unwrap();
let command_buffer = builder.build().unwrap();
if wait_for_last_frame {
if let Some(FrameEndFuture::FenceSignalFuture(ref mut f)) = previous_frame_end {
f.wait(None).unwrap();
}
}
let future = previous_frame_end
.take()
.unwrap()
.get()
.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(FrameEndFuture::FenceSignalFuture(future));
}
Err(FlushError::OutOfDate) => {
recreate_swapchain = true;
previous_frame_end = Some(FrameEndFuture::now(device.clone()));
}
Err(e) => {
println!("Failed to flush future: {:?}", e);
previous_frame_end = Some(FrameEndFuture::now(device.clone()));
}
}
}
_ => (),
}
});
}
fn window_size_dependent_setup(
images: &[Arc<SwapchainImage<Window>>],
render_pass: Arc<RenderPass>,
viewport: &mut Viewport,
) -> Vec<Arc<Framebuffer>> {
let dimensions = images[0].dimensions().width_height();
viewport.dimensions = [dimensions[0] as f32, dimensions[1] as f32];
images
.iter()
.map(|image| {
let view = ImageView::new_default(image.clone()).unwrap();
Framebuffer::new(
render_pass.clone(),
FramebufferCreateInfo {
attachments: vec![view],
..Default::default()
},
)
.unwrap()
})
.collect::<Vec<_>>()
}

View File

@ -1,147 +0,0 @@
//! Contains items that can be added to a plot.
use std::ops::RangeInclusive;
pub use arrows::*;
use egui::Pos2;
use epaint::{Color32, Shape, Stroke};
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_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
}

View File

@ -1,100 +0,0 @@
use std::ops::RangeInclusive;
use egui::Ui;
use epaint::{Color32, Shape};
use crate::items::plot_item::PlotItem;
use crate::items::values::Values;
use crate::transform::{Bounds, ScreenTransform};
/// 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,
}
}
}
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()
}
}

View File

@ -1,86 +0,0 @@
use std::ops::RangeInclusive;
use egui::Ui;
use epaint::{Color32, Shape, Stroke};
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};
/// 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,
}
}
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
/// be auto-assigned.
#[must_use]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.stroke.color = color.into();
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
}
}

View File

@ -1,132 +0,0 @@
use std::ops::RangeInclusive;
use egui::{pos2, NumExt, Ui};
use epaint::{Color32, Mesh, Rgba, Shape, Stroke};
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};
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,
}
}
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
/// be auto-assigned.
#[must_use]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.stroke.color = color.into();
self
}
}

View File

@ -1,33 +0,0 @@
#[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

@ -1,155 +0,0 @@
use std::ops::RangeInclusive;
use egui::{pos2, Image, Rect, Ui, Vec2};
use epaint::{Color32, Shape, Stroke, TextureId};
use crate::items::plot_item::PlotItem;
use crate::items::value::Value;
use crate::items::values::Values;
use crate::transform::{Bounds, ScreenTransform};
/// 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.
#[must_use]
pub fn highlight(mut self) -> Self {
self.highlight = true;
self
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
#[must_use]
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.
#[must_use]
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).
#[must_use]
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)]
#[must_use]
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

@ -1,19 +0,0 @@
use std::ops::RangeInclusive;
use egui::Ui;
use epaint::{Color32, Shape};
use crate::items::Values;
use crate::transform::{Bounds, ScreenTransform};
/// 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;
}

View File

@ -1,250 +0,0 @@
use std::ops::RangeInclusive;
use egui::{pos2, vec2, Pos2, Ui};
use epaint::{Color32, Shape, Stroke};
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};
/// 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.
#[must_use]
pub fn shape(mut self, shape: MarkerShape) -> Self {
self.shape = shape;
self
}
/// Highlight these points in the plot by scaling up their markers.
#[must_use]
pub fn highlight(mut self) -> Self {
self.highlight = true;
self
}
/// Set the marker's color.
#[must_use]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.color = color.into();
self
}
/// Whether to fill the marker.
#[must_use]
pub fn filled(mut self, filled: bool) -> Self {
self.filled = filled;
self
}
/// Whether to add stems between the markers and a horizontal reference
/// line.
#[must_use]
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.
#[must_use]
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)]
#[must_use]
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_some(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

@ -1,147 +0,0 @@
use std::ops::RangeInclusive;
use egui::{NumExt, Ui};
use epaint::{Color32, Rgba, Shape, Stroke};
use crate::items::plot_item::PlotItem;
use crate::items::values::Values;
use crate::items::{LineStyle, DEFAULT_FILL_ALPHA};
use crate::transform::{Bounds, ScreenTransform};
/// 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.
#[must_use]
pub fn highlight(mut self) -> Self {
self.highlight = true;
self
}
/// Add a custom stroke.
#[must_use]
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = stroke.into();
self
}
/// Set the stroke width.
#[must_use]
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.
#[must_use]
pub fn color(mut self, color: impl Into<Color32>) -> Self {
self.stroke.color = color.into();
self
}
/// Alpha of the filled area.
#[must_use]
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`.
#[must_use]
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)]
#[must_use]
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()
}
}

View File

@ -1,131 +0,0 @@
use std::ops::RangeInclusive;
use egui::{Align2, Rect, TextStyle, Ui};
use epaint::{Color32, Shape, Stroke};
use crate::items::plot_item::PlotItem;
use crate::items::value::Value;
use crate::items::values::Values;
use crate::transform::{Bounds, ScreenTransform};
/// 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.
#[must_use]
pub fn highlight(mut self) -> Self {
self.highlight = true;
self
}
/// Text style. Default is `TextStyle::Small`.
#[must_use]
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.
#[must_use]
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`.
#[must_use]
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)]
#[must_use]
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 fond_id = ui.style().text_styles.get(&self.style).unwrap();
let pos = transform.position_from_value(&self.position);
let galley = ui
.fonts()
.layout_no_wrap(self.text.clone(), fond_id.clone(), 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
}
}

View File

@ -1,127 +0,0 @@
use std::ops::RangeInclusive;
use egui::Ui;
use epaint::{Color32, Shape, Stroke};
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};
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.
#[must_use]
pub fn highlight(mut self) -> Self {
self.highlight = true;
self
}
/// Add a stroke.
#[must_use]
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
self.stroke = stroke.into();
self
}
/// Stroke width. A high value means the plot thickens.
#[must_use]
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.
#[must_use]
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`.
#[must_use]
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)]
#[must_use]
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
}

View File

@ -1,22 +0,0 @@
/// 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(),
}
}
}

View File

@ -1,139 +0,0 @@
use std::collections::Bound;
use std::ops::{RangeBounds, RangeInclusive};
use crate::items::{ExplicitGenerator, Value};
use crate::transform::Bounds;
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_some(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
}
}

View File

@ -1,44 +0,0 @@
use app::AmdGui;
use tokio::sync::mpsc::UnboundedReceiver;
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");
}
tracing_subscriber::fmt::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)));
let receiver = schedule_tick(amd_gui.clone());
backend::run_app(amd_gui, receiver);
}
fn schedule_tick(amd_gui: std::sync::Arc<parking_lot::Mutex<AmdGui>>) -> UnboundedReceiver<bool> {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
let sender = sender;
loop {
amd_gui.lock().tick();
if let Err(e) = sender.send(true) {
tracing::error!("Failed to propagate tick update. {:?}", e);
}
tokio::time::sleep(tokio::time::Duration::from_millis(166)).await;
}
});
receiver
}

View File

@ -1,244 +0,0 @@
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

@ -1,180 +0,0 @@
use amdgpu::pidfile::helper_cmd::{send_command, Command, Response};
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;
use crate::widgets::drag_plot::PlotMsg;
use crate::widgets::reload_section::ReloadSection;
use crate::widgets::ConfigFile;
pub struct ChangeFanSettings {
config: FanConfig,
selected: Option<usize>,
matrix: Vec<MatrixPoint>,
}
impl ChangeFanSettings {
pub fn new(config: FanConfig) -> Self {
let matrix = config.lock().speed_matrix().to_vec();
Self {
matrix,
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.temp, v.speed));
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("Speed")
.x_axis_name("Temperature")
.hline(crate::items::HLine::new(0.0).color(Color32::BLACK))
.hline(crate::items::HLine::new(100.0).color(Color32::BLACK))
.vline(crate::items::VLine::new(0.0).color(Color32::BLACK))
.vline(crate::items::VLine::new(100.0).color(Color32::BLACK))
.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((cache, current)) =
self.matrix.get_mut(idx).zip(current.as_deref())
{
cache.speed = current.speed;
cache.temp = current.temp;
}
if let Some(point) = current {
point.speed = (point.speed + delta.y as f64)
.max(min.speed)
.min(max.speed);
point.temp = (point.temp + delta.x as f64)
.max(min.temp)
.min(max.temp);
}
}
}
})
.legend(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(), &mut self.matrix));
});
});
}
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)*/);
}
}
});
}
#[allow(clippy::explicit_auto_deref)]
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) => {
tracing::error!("Config file serialization failed. {:?}", e);
return;
}
Ok(content) => content,
};
let command = Command::SaveFanConfig {
path: String::from(config.path()),
content,
};
match send_command(command) {
Ok(Response::ConfigFileSaveFailed(msg)) => {
state.state = ChangeState::Failure(msg);
}
Ok(Response::ConfigFileSaved) => {
state.state = ChangeState::Success;
}
_ => {}
}
}
}

View File

@ -1,107 +0,0 @@
use amdgpu_config::fan::MatrixPoint;
use egui::{PointerButton, Response, Sense, Ui, Widget};
use crate::app::FanConfig;
pub struct ConfigFile<'l> {
config: FanConfig,
matrix: &'l mut [MatrixPoint],
}
impl<'l> ConfigFile<'l> {
pub fn new(config: FanConfig, matrix: &'l mut [MatrixPoint]) -> Self {
Self { config, matrix }
}
}
impl<'l> Widget for ConfigFile<'l> {
fn ui(self, ui: &mut Ui) -> Response {
let config = self.config.clone();
ui.vertical(|ui| {
let mut matrix = self.matrix.iter_mut().enumerate().peekable();
let mut prev: Option<MatrixPoint> = None;
while let Some((idx, current)) = matrix.next() {
let min: MatrixPoint = if current == &MatrixPoint::MIN {
MatrixPoint::MIN
} else if let Some(prev) = &prev {
*prev
} else {
MatrixPoint::MIN
};
let next = matrix.peek();
let max: MatrixPoint = if current == &MatrixPoint::MAX {
MatrixPoint::MAX
} else if let Some(next) = next.map(|(_, n)| n) {
MatrixPoint::new(next.temp, next.speed)
} else {
MatrixPoint::MAX
};
{
ui.label("Speed");
if ui
.add(egui::Slider::new(&mut current.speed, min.speed..=max.speed))
.changed()
{
if let Some(entry) = config.lock().speed_matrix_mut().get_mut(idx) {
entry.speed = current.speed;
}
}
}
{
ui.label("Temperature");
if ui
.add(egui::Slider::new(&mut current.temp, min.temp..=max.temp))
.changed()
{
if let Some(entry) = config.lock().speed_matrix_mut().get_mut(idx) {
entry.temp = current.temp;
}
}
}
ui.horizontal(|ui| {
if next.is_some() {
if ui
.add(egui::Button::new("Add in the middle"))
.clicked_by(PointerButton::Primary)
{
config.lock().speed_matrix_vec_mut().insert(
idx + 1,
MatrixPoint::new(
min.speed + ((max.speed - min.speed) / 2.0),
min.temp + ((max.temp - min.temp) / 2.0),
),
)
}
} else if next.is_none()
&& *current != MatrixPoint::MAX
&& ui
.add(egui::Button::new("Add"))
.clicked_by(PointerButton::Primary)
{
config
.lock()
.speed_matrix_vec_mut()
.push(MatrixPoint::new(100.0, 100.0))
}
if ui
.add(egui::Button::new("Remove"))
.clicked_by(PointerButton::Primary)
{
config.lock().speed_matrix_vec_mut().remove(idx);
}
});
ui.separator();
prev = Some(*current);
}
ui.allocate_response(ui.available_size(), Sense::click())
})
.inner
}
}

View File

@ -1,95 +0,0 @@
use core::option::Option;
use core::option::Option::Some;
use std::collections::vec_deque::VecDeque;
use amdgpu::Card;
use amdmond_lib::AmdMon;
use egui::Ui;
use crate::app::{FanConfig, FanServices};
pub struct CoolingPerformance {
capacity: usize,
data: VecDeque<f64>,
amd_mon: Option<AmdMon>,
}
impl CoolingPerformance {
#[allow(clippy::explicit_auto_deref)]
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);
ui.label("Temperature");
Plot::new("cooling performance")
.allow_drag(false)
.allow_zoom(false)
.height(600.0)
.show(ui, |plot_ui| {
plot_ui.line(curve);
plot_ui.hline(zero);
plot_ui.hline(optimal);
plot_ui.hline(target);
// plot_ui.legend(Legend::default());
});
// 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

@ -1,441 +0,0 @@
use egui::{emath, vec2, CursorIcon, Id, NumExt, PointerButton, Response, Sense, Ui, Vec2};
use epaint::ahash::AHashSet;
use epaint::color::Hsva;
use epaint::{Color32, Rounding};
use crate::items::{HLine, *};
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),
}
// , serde::Serialize, serde::Deserialize
#[derive(Clone)]
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")],
}
}
#[must_use]
pub fn x_axis_name<S: Into<String>>(mut self, name: S) -> Self {
self.axis_names[0] = name.into();
self
}
#[must_use]
pub fn y_axis_name<S: Into<String>>(mut self, name: S) -> Self {
self.axis_names[1] = name.into();
self
}
/// Show a legend including all named items.
#[must_use]
pub fn legend(mut self, legend: Legend) -> Self {
self.legend_config = Some(legend);
self
}
/// Add a data lines.
#[must_use]
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
}
#[must_use]
pub fn selected(mut self, selected: Option<usize>) -> Self {
self.selected = selected;
self
}
#[must_use]
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.
#[must_use]
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).
#[must_use]
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.
#[must_use]
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.
#[must_use]
pub fn height(mut self, height: f32) -> Self {
self.min_size.y = height;
self.height = Some(height);
self
}
/// Minimum size of the plot view.
#[must_use]
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
#[must_use]
pub fn allow_drag(mut self, allow_drag: bool) -> Self {
self.allow_drag = allow_drag;
self
}
#[must_use]
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.
#[must_use]
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.
#[must_use]
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
}
#[must_use]
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
.ctx()
.data()
.get_persisted_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().with_clip_rect(rect);
plot_painter.add(epaint::RectShape {
rect,
rounding: Rounding::from(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 t_bounds = *transform.bounds();
let prepared = DragPlotPrepared {
items,
lines,
show_x: true,
show_y: true,
show_axes: [true, true],
transform: transform.clone(),
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)
&& response.hover_pos().is_some()
{
let mut delta = response.drag_delta();
delta.x *= transform.dvalue_dpos()[0] as f32;
delta.y *= transform.dvalue_dpos()[1] as f32;
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.ctx()
.data()
.get_persisted_mut_or_insert_with(plot_id, || PlotMemory {
bounds: t_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

@ -1,277 +0,0 @@
use egui::{emath, pos2, remap_clamp, vec2, Align2, Layout, NumExt, Pos2, Response, TextStyle, Ui};
use epaint::{Color32, Rgba, Shape, Stroke};
use crate::items::{Line, PlotItem, Value};
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()
.with_clip_rect(*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,
ui.style().text_styles.get(&text_style).cloned().unwrap(),
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 values in lines.iter().filter_map(|v| v.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!()
}
};
#[allow(clippy::explicit_auto_deref)]
shapes.push(Shape::text(
&*ui.fonts(),
pointer + vec2(3.0, -2.0),
Align2::LEFT_BOTTOM,
text,
ui.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap(),
ui.visuals().text_color(),
));
}
}

View File

@ -1,161 +0,0 @@
use std::string::String;
use egui::{
pos2, vec2, Align, PointerButton, Rect, Response, Sense, TextStyle, WidgetInfo, WidgetType,
};
use epaint::Color32;
/// Where to place the plot legend.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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, 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`.
#[must_use]
pub fn text_style(mut self, style: TextStyle) -> Self {
self.text_style = style;
self
}
/// The alpha of the legend background. Default: `0.75`.
#[must_use]
pub fn background_alpha(mut self, alpha: f32) -> Self {
self.background_alpha = alpha;
self
}
/// In which corner to place the legend. Default: `Corner::RightTop`.
#[must_use]
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()
.text_styles
.get(&TextStyle::Body)
.unwrap()
.clone(),
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

@ -1,115 +0,0 @@
use std::collections::BTreeMap;
use egui::style::Margin;
use egui::{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_some(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 {
inner_margin: Margin::symmetric(8.0, 4.0),
outer_margin: Default::default(),
rounding: ui.style().visuals.window_rounding,
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

@ -1,14 +0,0 @@
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 output_widget;
pub mod outputs_settings;
pub mod reload_section;
pub use change_fan_settings::*;
pub use config_file::*;
pub use cooling_performance::*;

View File

@ -1,58 +0,0 @@
use amdgpu::pidfile::ports::Output;
use egui::{Response, Sense, Ui, Vec2};
use epaint::Stroke;
use crate::app::StatefulConfig;
pub struct OutputWidget<'output, 'stateful> {
output: &'output Output,
state: &'stateful mut StatefulConfig,
}
impl<'output, 'stateful> OutputWidget<'output, 'stateful> {
pub fn new(output: &'output Output, state: &'stateful mut StatefulConfig) -> Self {
Self { output, state }
}
}
impl<'output, 'stateful> egui::Widget for OutputWidget<'output, 'stateful> {
fn ui(self, ui: &mut Ui) -> Response {
let (rect, res) = ui.allocate_exact_size(Vec2::new(80.0, 80.0), Sense::click());
if let Some(handle) = self.output.ty.and_then(|ty| self.state.textures.get(&ty)) {
ui.image(handle.id(), handle.size_vec2());
} else {
let painter = ui.painter();
painter.rect_filled(rect, 0.0, epaint::color::Color32::DARK_RED);
painter.rect(rect, 2.0, epaint::color::Color32::DARK_RED, {
Stroke {
width: 1.0,
color: epaint::color::Color32::GREEN,
}
});
let rect_middle_point = (rect.max - rect.min) / 2.0;
painter.circle_filled(
rect.min + Vec2::new(rect_middle_point.x / 2.0, rect_middle_point.y),
3.0,
epaint::color::Color32::GREEN,
);
painter.circle_filled(
rect.min + rect_middle_point,
3.0,
epaint::color::Color32::GREEN,
);
painter.circle_filled(
rect.min
+ Vec2::new(
rect_middle_point.x + (rect_middle_point.x / 2.0),
rect_middle_point.y,
),
3.0,
epaint::color::Color32::GREEN,
);
}
res
}
}

View File

@ -1,77 +0,0 @@
use std::collections::BTreeMap;
use amdgpu::pidfile::ports::{Output, Status};
use egui::{RichText, Ui, WidgetText};
use epaint::Color32;
use crate::app::StatefulConfig;
use crate::widgets::output_widget::OutputWidget;
#[derive(Default)]
pub struct OutputsSettings {}
impl OutputsSettings {
pub fn draw(
&mut self,
ui: &mut Ui,
state: &mut StatefulConfig,
outputs: &BTreeMap<String, Vec<Output>>,
) {
let _available = ui.available_rect_before_wrap();
ui.vertical(|ui| {
ui.horizontal_top(|ui| {
outputs.iter().for_each(|(name, outputs)| {
ui.vertical(|ui| {
ui.label(format!("Card {name}"));
ui.horizontal_top(|ui| {
outputs.iter().for_each(|output| {
Self::render_single(ui, state, output);
});
});
});
});
});
});
}
fn render_single(ui: &mut Ui, state: &mut StatefulConfig, output: &Output) {
ui.vertical(|ui| {
ui.add(OutputWidget::new(output, state));
ui.label(format!("Port type {:?}", output.port_type));
ui.label(format!("Port number {}", output.port_number));
if let Some(name) = output.port_name.as_deref() {
ui.label(format!("Port name {}", name));
}
ui.label(WidgetText::RichText(
RichText::new(match output.status {
Status::Connected => "Connected",
Status::Disconnected => "Disconnected",
})
.color(match output.status {
Status::Connected => Color32::GREEN,
Status::Disconnected => Color32::GRAY,
})
.code()
.strong()
.monospace(),
));
ui.label("Display Power Management");
ui.label(WidgetText::RichText(
RichText::new(match output.display_power_managment {
true => "On",
false => "Off",
})
.color(match output.display_power_managment {
true => Color32::GREEN,
false => Color32::GRAY,
})
.monospace()
.code()
.strong(),
));
});
}
}

View File

@ -1,61 +0,0 @@
use amdgpu::pidfile::helper_cmd::Command;
use egui::{PointerButton, Response, Sense, Ui};
use crate::app::{ChangeState, FanServices};
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::pidfile::helper_cmd::send_command(Command::ReloadConfig {
pid: service.pid,
}) {
Ok(response) => {
service.reload = ChangeState::Success;
tracing::info!("{:?}", response)
}
Err(e) => {
service.reload = ChangeState::Failure(format!("{:?}", e));
tracing::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 }
}
}

View File

@ -1,29 +0,0 @@
[package]
name = "amdmond-lib"
version = "1.0.10"
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.11" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", 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"] }

View File

@ -1,26 +0,0 @@
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,
}

View File

@ -1,118 +0,0 @@
pub mod errors;
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;
use crate::errors::AmdMonError;
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,31 +0,0 @@
[package]
name = "amdmond"
version = "1.0.10"
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.11" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["monitor", "fan"] }
amdmond-lib = { path = "../amdmond-lib", version = "1.0.10" }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
csv = { version = "1.1.6" }
thiserror = { version = "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"] }
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }

View File

@ -1,66 +0,0 @@
# AMD Monitoring daemon
## Watch mode
Tool will check temperature and prints:
* Minimal modulation
* Maximal modulation
* Current modulation
* Current fan speed in percentage (PWD / PWD MAX * 100)
* Current value of each temperature sensor (typically temp1_input is which should be observed)
> `modulation` is a value between 0-255 which indicate how fast fan should be moving
```bash
/usr/bin/amdmond watch --format short
```
### Formats
There are 2 possible formats.
* `short` - very compact info
* `long` - more human-readable info
## Log File mode
This tool can be used to track GPU temperature and amdfand speed curve management to prevent GPU card from generating
unnecessary noise.
It will create csv log file with:
* time
* temperature
* card modulation
* matrix point temperature
* matrix point speed
```bash
/usr/bin/amdmond log_file -s /var/log/amdmon.csv
```
## Install
```bash
cargo install amdmond
```
## Usage
### minimal:
```
amdmond log_file -s /var/log/amdmon.csv
```
Required arguments:
* `-s`, `--stat-file STAT-FILE` Full path to statistics file
Optional arguments:
* `-h`, `--help` Help message
* `-v`, `--version` Print version
* `-c`, `--config CONFIG` Config location
* `-i`, `--interval INTERVAL` Time between each check. 1000 is 1s, by default 5s

View File

@ -1,13 +0,0 @@
use crate::{log_file, watch};
#[derive(gumdrop::Options)]
pub enum Command {
Watch(watch::Watch),
LogFile(log_file::LogFile),
}
impl Default for Command {
fn default() -> Self {
Self::Watch(watch::Watch::default())
}
}

View File

@ -1,98 +0,0 @@
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 {
#[options(help = "Help message")]
help: bool,
#[options(help = "Full path to statistics file")]
stat_file: String,
#[options(help = "Time between each check. 1000 is 1s, by default 5s")]
interval: Option<u32>,
}
#[derive(serde::Serialize)]
struct Stat {
time: chrono::DateTime<chrono::Local>,
temperature: f64,
modulation: u32,
speed_setting: f64,
temperature_setting: f64,
}
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(
0,
command.interval.unwrap_or_else(|| config.interval()) * 1_000_000,
);
log::info!("Updating each: {:?}", duration);
let _ = std::fs::remove_file(command.stat_file.as_str());
let stat_file = std::fs::OpenOptions::new()
.create(true)
.append(false)
.write(true)
.open(command.stat_file.as_str())?;
let mut writer = csv::WriterBuilder::new()
.double_quote(true)
.has_headers(true)
.buffer_capacity(100)
.from_writer(stat_file);
let mon = {
let mons = hw_mons(true)?;
if mons.is_empty() {
return Err(AmdMonError::NoHwMon);
}
AmdMon::wrap(hw_mons(true)?.remove(0), &fan_config)
};
loop {
let time = chrono::Local::now();
let temperature = {
let mut temperatures = mon.gpu_temp();
if let Some(input) = fan_config.temp_input() {
let input_name = input.as_string();
temperatures
.into_iter()
.find(|(name, _value)| name == &input_name)
.and_then(|(_, v)| v.ok())
.unwrap_or_default()
} else if temperatures.len() > 1 {
temperatures.remove(1).1.unwrap_or_default()
} else {
temperatures
.get(0)
.and_then(|(_, v)| v.as_ref().ok().copied())
.unwrap_or_default()
}
};
let (speed_setting, temperature_setting) =
if let Some(speed_matrix_point) = fan_config.speed_matrix_point(temperature) {
(speed_matrix_point.speed, speed_matrix_point.temp)
} else {
Default::default()
};
let stat = Stat {
time,
temperature,
modulation: mon.pwm()?,
speed_setting,
temperature_setting,
};
writer.serialize(stat)?;
writer.flush()?;
std::thread::sleep(duration);
}
}

View File

@ -1,71 +0,0 @@
mod command;
mod log_file;
mod watch;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
use gumdrop::Options;
use crate::command::Command;
#[derive(gumdrop::Options)]
pub struct Opts {
#[options(help = "Help message")]
pub help: bool,
#[options(help = "Print version")]
pub version: bool,
#[options(help = "Config location")]
pub config: Option<String>,
#[options(command)]
pub command: Option<Command>,
}
fn run(config: Config) -> amdmond_lib::Result<()> {
let opts: Opts = Opts::parse_args_default_or_exit();
if opts.version {
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
match opts.command {
Some(Command::Watch(w)) => watch::run(w, config),
Some(Command::LogFile(l)) => log_file::run(l, config),
_ => {
println!("{}", <Opts as gumdrop::Options>::usage());
Ok(())
}
}
}
fn setup() -> amdmond_lib::Result<(String, Config)> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "DEBUG");
}
pretty_env_logger::init();
ensure_config_dir()?;
let config_path = Opts::parse_args_default_or_exit()
.config
.unwrap_or_else(|| DEFAULT_MONITOR_CONFIG_PATH.to_string());
let config = load_config(&config_path)?;
log::info!("{:?}", config);
log::set_max_level(config.log_level().as_str().parse().unwrap());
Ok((config_path, config))
}
fn main() -> amdmond_lib::Result<()> {
let (_config_path, config) = match setup() {
Ok(config) => config,
Err(e) => {
log::error!("{}", e);
std::process::exit(1);
}
};
match run(config) {
Ok(()) => Ok(()),
Err(e) => {
log::error!("{}", e);
std::process::exit(1);
}
}
}

View File

@ -1,15 +0,0 @@
[package]
name = "amdportsd"
version = "0.1.0"
edition = "2021"
[dependencies]
amdgpu = { path = "../amdgpu", features = ["gui-helper"] }
tokio = { version = "1.19.2", features = ["full"] }
futures = { version = "0.3", features = [] }
ron = { version = "0.7.1" }
serde = { version = "1.0.137", features = ["derive"] }
tracing = { version = "0.1.36" }
tracing-subscriber = { version = "0.3.15" }

View File

@ -1,156 +0,0 @@
use std::fs::{DirEntry, Permissions};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use amdgpu::pidfile::ports::{sock_file, Command, Response, *};
use amdgpu::IoFailure;
fn parse_output(entry: DirEntry) -> Option<Output> {
let ty = entry.file_type().ok()?;
if ty.is_dir() {
return None;
}
tracing::error!("{:?}", entry.path());
let file_name = entry.file_name();
let path = file_name.to_str()?;
let mut it = path
.split('-')
.map(String::from)
.collect::<Vec<_>>()
.into_iter();
let modes = std::fs::read_to_string(entry.path().join("modes"))
.unwrap_or_default()
.lines()
.filter_map(|s| {
let mut it = s.split('x');
let width = it.next().and_then(|s| s.parse::<u16>().ok())?;
let height = it.next().and_then(|s| s.parse::<u16>().ok())?;
Some(OutputMode { width, height })
})
.collect::<Vec<_>>();
let card = it.next()?.strip_prefix("card")?.to_string();
let port_type = it.next()?;
let dpms = std::fs::read_to_string(entry.path().join("dpms"))
.unwrap_or_else(|e| {
tracing::error!("{}", e);
"Off".into()
})
.to_lowercase();
tracing::info!("Display Power Management System is {:?}", dpms);
let mut output = Output {
card,
ty: OutputType::parse_str(&port_type),
port_type,
modes,
display_power_managment: dpms.trim() == "on",
..Default::default()
};
let mut it = it.rev();
output.port_number = it.next()?.parse().ok()?;
let mut it = it.rev().peekable();
if it.peek().is_some() {
output.port_name = Some(it.collect::<Vec<_>>().join("-"));
}
output.status = output.read_status()?;
Some(output)
}
async fn read_outputs(state: Arc<Mutex<Vec<Output>>>) {
loop {
let outputs = std::fs::read_dir("/sys/class/drm")
.unwrap()
.filter_map(|r| r.ok())
.filter(|e| {
e.path()
.to_str()
.map(|s| s.contains("card"))
.unwrap_or_default()
})
.filter_map(parse_output)
.collect::<Vec<_>>();
if let Ok(mut lock) = state.lock() {
*lock = outputs;
}
tokio::time::sleep(Duration::from_millis(1_000 / 3)).await;
}
}
pub type Service = amdgpu::pidfile::Service<Response>;
async fn service(state: Arc<Mutex<Vec<Output>>>) {
let sock_path = sock_file();
let listener = {
let _ = std::fs::remove_file(&sock_path);
UnixListener::bind(&sock_path)
.map_err(|io| IoFailure {
io,
path: sock_path.clone(),
})
.expect("Creating pid file for ports failed")
};
if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) {
tracing::error!("Failed to change gui helper socket file mode. {:?}", e);
}
while let Ok((stream, _addr)) = listener.accept() {
handle_connection(stream, state.clone());
}
}
fn handle_connection(stream: UnixStream, state: Arc<Mutex<Vec<Output>>>) {
let mut service = Service::new(stream);
let command = match service.read_command() {
Some(s) => s,
_ => return service.kill(),
};
tracing::info!("Incoming {:?}", command);
let cmd = match ron::from_str::<Command>(command.trim()) {
Ok(cmd) => cmd,
Err(e) => {
tracing::warn!("Invalid message {:?}. {:?}", command, e);
return service.kill();
}
};
handle_command(service, cmd, state);
}
fn handle_command(mut service: Service, cmd: Command, state: Arc<Mutex<Vec<Output>>>) {
match cmd {
Command::Ports => {
if let Ok(outputs) = state.lock() {
service.write_response(Response::Ports(outputs.iter().map(Clone::clone).collect()));
}
}
}
}
fn main() {
tracing_subscriber::fmt::init();
let executor = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let state = Arc::new(Mutex::new(Vec::new()));
executor.block_on(async {
let sync = read_outputs(state.clone());
let handle = service(state);
tokio::join!(sync, handle);
});
}

View File

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

View File

@ -1,25 +0,0 @@
[package]
name = "amdvold"
version = "1.0.10"
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.11" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["voltage"] }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["voltage"] }

View File

@ -1,51 +0,0 @@
# AMD graphic card voltage manager
This tool can be used to overclock you AMD graphic card on Linux
## Install
```bash
cargo install amdvold
```
## Usage
Available commands:
* `setup-info` - prints information how to enable voltage management on Linux (see Requirements)
* `print-states` - prints current card states
* `change-state` - change card voltage states
* `apply-changes` - apply changes
## Changing states
Positional arguments:
* `index` Profile number
* `module` Either memory or engine
* `frequency` New GPU module frequency
* `voltage` New GPU module voltage
Optional arguments:
* `-a`, `--apply-immediately` Apply changes immediately after change
Example:
```bash
amdvold 1 engine 1450MHz 772mV
```
## Requirements
To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following:
* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example:
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1"
Easiest way is to modify "/etc/default/grub" and generate new grub config.
* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example:
options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff
(only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff")

View File

@ -1,13 +0,0 @@
To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following:
* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example:
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1"
Easiest way is to modify "/etc/default/grub" and generate new grub config.
* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example:
options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff
(only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff")

View File

@ -1,19 +0,0 @@
use amdgpu::utils::hw_mons;
use crate::command::VoltageManipulator;
use crate::{Config, VoltageError};
#[derive(Debug, gumdrop::Options)]
pub struct ApplyChanges {
help: bool,
}
pub fn run(_command: ApplyChanges, config: &Config) -> crate::Result<()> {
let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config);
if mons.is_empty() {
return Err(VoltageError::NoAmdGpu);
}
let mon = mons.remove(0);
mon.write_apply()?;
Ok(())
}

View File

@ -1,60 +0,0 @@
use amdgpu::utils::hw_mons;
use crate::clock_state::{Frequency, Voltage};
use crate::command::{HardwareModule, VoltageManipulator};
use crate::{Config, VoltageError};
#[derive(Debug, thiserror::Error)]
pub enum ChangeStateError {
#[error("No profile index was given")]
Index,
#[error("No frequency was given")]
Freq,
#[error("No voltage was given")]
Voltage,
#[error("No AMD GPU module was given (either memory or engine)")]
Module,
}
#[derive(Debug, gumdrop::Options)]
pub struct ChangeState {
#[options(help = "Help message")]
help: bool,
#[options(help = "Profile number", free)]
index: u16,
#[options(help = "Either memory or engine", free)]
module: Option<HardwareModule>,
#[options(help = "New GPU module frequency", free)]
frequency: Option<Frequency>,
#[options(help = "New GPU module voltage", free)]
voltage: Option<Voltage>,
#[options(help = "Apply changes immediately after change")]
apply_immediately: bool,
}
pub fn run(command: ChangeState, config: &Config) -> crate::Result<()> {
let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config);
if mons.is_empty() {
return Err(VoltageError::NoAmdGpu);
}
let mon = mons.remove(0);
let ChangeState {
help: _,
index,
module,
frequency,
voltage,
apply_immediately,
} = command;
mon.write_state(
index,
frequency.ok_or(ChangeStateError::Freq)?,
voltage.ok_or(ChangeStateError::Voltage)?,
module.ok_or(ChangeStateError::Module)?,
)?;
if apply_immediately {
mon.write_apply()?;
}
Ok(())
}

View File

@ -1,432 +0,0 @@
use std::iter::Peekable;
use std::str::Chars;
const ENGINE_CLOCK_LABEL: &str = "OD_SCLK:";
const MEMORY_CLOCK_LABEL: &str = "OD_MCLK:";
const CURVE_POINTS_LABEL: &str = "OD_VDDC_CURVE:";
#[derive(Debug, PartialEq, Eq)]
pub struct Frequency {
pub value: u32,
pub unit: String,
}
impl ToString for Frequency {
fn to_string(&self) -> String {
format!("{}{}", self.value, self.unit)
}
}
impl std::str::FromStr for Frequency {
type Err = ClockStateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut buffer = String::with_capacity(8);
let mut value = None;
for c in s.trim().chars() {
if c.is_numeric() && value.is_none() {
buffer.push(c);
} else if c.is_numeric() {
return Err(ClockStateError::NotFrequency(s.to_string()));
} else if value.is_none() {
if buffer.is_empty() {
return Err(ClockStateError::NotFrequency(s.to_string()));
}
value = Some(buffer.parse()?);
buffer.clear();
buffer.push(c);
} else {
buffer.push(c);
}
}
let value = value.ok_or_else(|| ClockStateError::NotFrequency(s.to_string()))?;
if !buffer.ends_with("hz") && !buffer.ends_with("Hz") {
return Err(ClockStateError::NotFrequency(s.to_string()));
}
Ok(Self {
value,
unit: buffer,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Voltage {
pub value: u32,
pub unit: String,
}
impl ToString for Voltage {
fn to_string(&self) -> String {
format!("{}{}", self.value, self.unit)
}
}
impl std::str::FromStr for Voltage {
type Err = ClockStateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut buffer = String::with_capacity(8);
let mut value = None;
for c in s.trim().chars() {
if c.is_numeric() && value.is_none() {
buffer.push(c);
} else if c.is_numeric() {
return Err(ClockStateError::NotVoltage(s.to_string()));
} else if value.is_none() {
if buffer.is_empty() {
return Err(ClockStateError::NotVoltage(s.to_string()));
}
value = Some(buffer.parse()?);
buffer.clear();
buffer.push(c);
} else {
buffer.push(c);
}
}
let value = value.ok_or_else(|| ClockStateError::NotVoltage(s.to_string()))?;
if !buffer.ends_with('V') {
return Err(ClockStateError::NotVoltage(s.to_string()));
}
Ok(Self {
value,
unit: buffer,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct CurvePoint {
pub freq: Frequency,
pub voltage: Voltage,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ClockStateError {
#[error("Can't parse value. {0:?}")]
ParseValue(#[from] std::num::ParseIntError),
#[error("Value {0:?} is not a voltage")]
NotVoltage(String),
#[error("Value {0:?} is not a frequency")]
NotFrequency(String),
#[error("Voltage section for engine clock is not valid. Line {0:?} is malformed")]
InvalidEngineClockSection(String),
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClockState {
pub curve_labels: Vec<CurvePoint>,
pub engine_label_lowest: Option<Frequency>,
pub engine_label_highest: Option<Frequency>,
pub memory_label_lowest: Option<Frequency>,
pub memory_label_highest: Option<Frequency>,
}
impl Default for ClockState {
fn default() -> Self {
Self {
curve_labels: Vec::with_capacity(3),
engine_label_lowest: None,
engine_label_highest: None,
memory_label_lowest: None,
memory_label_highest: None,
}
}
}
impl std::str::FromStr for ClockState {
type Err = ClockStateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut clock_state = Self::default();
enum State {
Unknown,
ParseEngineClock,
ParseMemoryClock,
ParseCurve,
}
let mut state = State::Unknown;
for line in s.lines() {
let start = match line.chars().position(|c| c != ' ' && c != '\0') {
Some(idx) => idx,
_ => continue,
};
let line = line[start..].trim();
match state {
_ if line == "OD_RANGE:" => break,
_ if line == ENGINE_CLOCK_LABEL => {
state = State::ParseEngineClock;
}
_ if line == MEMORY_CLOCK_LABEL => {
state = State::ParseMemoryClock;
}
_ if line == CURVE_POINTS_LABEL => {
state = State::ParseCurve;
}
State::ParseEngineClock => {
if clock_state.engine_label_lowest.is_none() {
clock_state.engine_label_lowest = Some(parse_freq_line(line)?);
} else {
clock_state.engine_label_highest = Some(parse_freq_line(line)?);
}
}
State::ParseMemoryClock => {
if clock_state.memory_label_lowest.is_none() {
clock_state.memory_label_lowest = Some(parse_freq_line(line)?);
} else {
clock_state.memory_label_highest = Some(parse_freq_line(line)?);
}
}
State::ParseCurve => {
let (freq, volt) = parse_freq_voltage_line(line)?;
clock_state.curve_labels.push(CurvePoint {
freq,
voltage: volt,
});
}
_ => {}
}
}
Ok(clock_state)
}
}
fn consume_mode_number<'line>(
line: &'line str,
chars: &mut Peekable<Chars<'line>>,
) -> std::result::Result<(), ClockStateError> {
let mut buffer = String::with_capacity(4);
while chars.peek().filter(|c| c.is_numeric()).is_some() {
buffer.push(chars.next().unwrap());
}
if buffer.is_empty() {
return Err(ClockStateError::InvalidEngineClockSection(line.to_string()));
}
chars
.next()
.filter(|c| *c == ':')
.ok_or_else(|| ClockStateError::InvalidEngineClockSection(line.to_string()))?;
Ok(())
}
fn consume_freq(chars: &mut Peekable<Chars>) -> std::result::Result<Frequency, ClockStateError> {
consume_white(chars);
chars
.take_while(|c| *c != ' ')
.collect::<String>()
.parse::<Frequency>()
}
fn consume_voltage(chars: &mut Peekable<Chars>) -> std::result::Result<Voltage, ClockStateError> {
consume_white(chars);
chars
.take_while(|c| *c != ' ')
.collect::<String>()
.parse::<Voltage>()
}
fn consume_white(chars: &mut Peekable<Chars>) {
while chars.peek().filter(|c| **c == ' ').is_some() {
let _ = chars.next();
}
}
fn parse_freq_line(line: &str) -> std::result::Result<Frequency, ClockStateError> {
let mut chars = line.chars().peekable();
consume_mode_number(line, &mut chars)?;
consume_freq(&mut chars)
}
fn parse_freq_voltage_line(
line: &str,
) -> std::result::Result<(Frequency, Voltage), ClockStateError> {
let mut chars = line.chars().peekable();
consume_mode_number(line, &mut chars)?;
let freq = consume_freq(&mut chars)?;
consume_white(&mut chars);
Ok((freq, consume_voltage(&mut chars)?))
}
#[cfg(test)]
mod parse_frequency {
use crate::clock_state::{ClockStateError, Frequency};
#[test]
fn parse_empty_string() {
assert_eq!(
"".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("".to_string()))
);
}
#[test]
fn parse_only_v_letter() {
assert_eq!(
"v".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("v".to_string()))
);
}
#[test]
fn parse_only_hz() {
assert_eq!(
"hz".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("hz".to_string()))
);
}
#[test]
fn parse_only_mhz() {
assert_eq!(
"Mhz".parse::<Frequency>(),
Err(ClockStateError::NotFrequency("Mhz".to_string()))
);
}
#[test]
fn parse_0mhz() {
assert_eq!(
"0Mhz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "Mhz".to_string(),
})
);
}
#[test]
fn parse_0khz() {
assert_eq!(
"0khz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "khz".to_string(),
})
);
}
#[test]
fn parse_0kz() {
assert_eq!(
"0hz".parse::<Frequency>(),
Ok(Frequency {
value: 0,
unit: "hz".to_string(),
})
);
}
#[test]
fn parse_123mhz() {
assert_eq!(
"123Mhz".parse::<Frequency>(),
Ok(Frequency {
value: 123,
unit: "Mhz".to_string(),
})
);
}
#[test]
fn parse_123khz() {
assert_eq!(
"123khz".parse::<Frequency>(),
Ok(Frequency {
value: 123,
unit: "khz".to_string(),
})
);
}
#[test]
fn parse_123kz() {
assert_eq!(
"123hz".parse::<Frequency>(),
Ok(Frequency {
value: 123,
unit: "hz".to_string(),
})
);
}
}
#[cfg(test)]
mod state_tests {
use crate::clock_state::{ClockState, CurvePoint, Frequency, Voltage};
#[test]
fn valid_string() {
let s = r#"
OD_SCLK:
0: 800Mhz
1: 2100Mhz
OD_MCLK:
1: 875MHz
OD_VDDC_CURVE:
0: 800MHz 706mV
1: 1450MHz 772mV
2: 2100MHz 1143mV
OD_RANGE:
SCLK: 800Mhz 2150Mhz
MCLK: 625Mhz 950Mhz
VDDC_CURVE_SCLK[0]: 800Mhz 2150Mhz
VDDC_CURVE_VOLT[0]: 750mV 1200mV
VDDC_CURVE_SCLK[1]: 800Mhz 2150Mhz
VDDC_CURVE_VOLT[1]: 750mV 1200mV
VDDC_CURVE_SCLK[2]: 800Mhz 2150Mhz
VDDC_CURVE_VOLT[2]: 750mV 1200mV
"#;
let res = s.trim().parse::<ClockState>();
assert_eq!(
res,
Ok(ClockState {
curve_labels: vec![
CurvePoint {
freq: Frequency {
value: 800,
unit: "MHz".to_string(),
},
voltage: Voltage {
value: 706,
unit: "mV".to_string(),
},
},
CurvePoint {
freq: Frequency {
value: 1450,
unit: "MHz".to_string(),
},
voltage: Voltage {
value: 772,
unit: "mV".to_string(),
},
},
CurvePoint {
freq: Frequency {
value: 2100,
unit: "MHz".to_string(),
},
voltage: Voltage {
value: 1143,
unit: "mV".to_string(),
},
},
],
engine_label_lowest: Some(Frequency {
value: 800,
unit: "Mhz".to_string(),
}),
engine_label_highest: Some(Frequency {
value: 2100,
unit: "Mhz".to_string(),
}),
memory_label_lowest: Some(Frequency {
value: 875,
unit: "MHz".to_string(),
}),
memory_label_highest: None,
})
);
}
}

View File

@ -1,97 +0,0 @@
use amdgpu::hw_mon::HwMon;
use crate::apply_changes::ApplyChanges;
use crate::change_state::ChangeState;
use crate::clock_state::{ClockState, Frequency, Voltage};
use crate::print_states::PrintStates;
use crate::setup_info::SetupInfo;
use crate::{Config, VoltageError};
#[derive(Debug)]
pub enum HardwareModule {
Engine,
Memory,
}
impl std::str::FromStr for HardwareModule {
type Err = VoltageError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"memory" => Ok(HardwareModule::Memory),
"engine" => Ok(HardwareModule::Engine),
_ => Err(VoltageError::UnknownHardwareModule(s.to_string())),
}
}
}
#[derive(Debug, gumdrop::Options)]
pub enum VoltageCommand {
SetupInfo(SetupInfo),
PrintStates(PrintStates),
ChangeState(ChangeState),
ApplyChanges(ApplyChanges),
}
pub struct VoltageManipulator {
hw_mon: HwMon,
}
impl std::ops::Deref for VoltageManipulator {
type Target = HwMon;
fn deref(&self) -> &Self::Target {
&self.hw_mon
}
}
impl std::ops::DerefMut for VoltageManipulator {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.hw_mon
}
}
impl VoltageManipulator {
pub fn wrap(hw_mon: HwMon, _config: &Config) -> Self {
Self { hw_mon }
}
pub fn wrap_all(mons: Vec<HwMon>, config: &Config) -> Vec<Self> {
mons.into_iter()
.map(|mon| Self::wrap(mon, config))
.collect()
}
pub fn write_apply(&self) -> crate::Result<()> {
self.device_write("pp_od_clk_voltage", "c")?;
Ok(())
}
pub fn write_state(
&self,
state_index: u16,
freq: Frequency,
voltage: Voltage,
module: HardwareModule,
) -> crate::Result<()> {
self.device_write(
"pp_od_clk_voltage",
format!(
"{module} {state_index} {freq} {voltage}",
state_index = state_index,
freq = freq.to_string(),
voltage = voltage.to_string(),
module = match module {
HardwareModule::Engine => "s",
HardwareModule::Memory => "m",
},
),
)?;
Ok(())
}
pub fn clock_states(&self) -> crate::Result<ClockState> {
let state = self.device_read("pp_od_clk_voltage")?.parse()?;
Ok(state)
}
}

View File

@ -1,25 +0,0 @@
use amdgpu::{utils, AmdGpuError};
use amdgpu_config::voltage::ConfigError;
use crate::change_state::ChangeStateError;
use crate::clock_state::ClockStateError;
#[derive(Debug, thiserror::Error)]
pub enum VoltageError {
#[error("No AMD GPU card was found")]
NoAmdGpu,
#[error("Unknown hardware module {0:?}")]
UnknownHardwareModule(String),
#[error("{0}")]
AmdGpu(AmdGpuError),
#[error("{0}")]
Config(#[from] ConfigError),
#[error("{0:}")]
Io(#[from] std::io::Error),
#[error("{0:}")]
ClockState(#[from] ClockStateError),
#[error("{0:}")]
ChangeStateError(#[from] ChangeStateError),
#[error("{0:}")]
AmdUtils(#[from] utils::AmdGpuError),
}

View File

@ -1,86 +0,0 @@
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::voltage::{load_config, Config};
use gumdrop::Options;
use crate::command::VoltageCommand;
use crate::error::VoltageError;
mod apply_changes;
mod change_state;
mod clock_state;
mod command;
mod error;
mod print_states;
mod setup_info;
pub static DEFAULT_CONFIG_PATH: &str = "/etc/amdfand/voltage.toml";
pub type Result<T> = std::result::Result<T, VoltageError>;
#[derive(gumdrop::Options)]
pub struct Opts {
#[options(help = "Help message")]
help: bool,
#[options(help = "Print version")]
version: bool,
#[options(help = "Config location")]
config: Option<String>,
#[options(command)]
command: Option<command::VoltageCommand>,
}
fn run(config: Config) -> Result<()> {
let opts: Opts = Opts::parse_args_default_or_exit();
if opts.version {
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
match opts.command {
None => {
Opts::usage();
Ok(())
}
Some(VoltageCommand::PrintStates(command)) => print_states::run(command, config),
Some(VoltageCommand::SetupInfo(command)) => setup_info::run(command, &config),
Some(VoltageCommand::ChangeState(command)) => change_state::run(command, &config),
Some(VoltageCommand::ApplyChanges(command)) => apply_changes::run(command, &config),
}
}
fn setup() -> Result<(String, Config)> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "DEBUG");
}
pretty_env_logger::init();
ensure_config_dir()?;
let config_path = Opts::parse_args_default_or_exit()
.config
.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
let config = load_config(&config_path)?;
log::set_max_level(config.log_level().as_str().parse().unwrap());
Ok((config_path, config))
}
fn main() -> Result<()> {
let (config_path, config) = match setup() {
Ok(config) => config,
Err(e) => {
log::error!("{}", e);
std::process::exit(1);
}
};
match run(config) {
Ok(()) => Ok(()),
Err(e) => {
let _config = load_config(&config_path).expect(
"Unable to restore automatic voltage control due to unreadable config file",
);
// panic_handler::restore_automatic(config);
log::error!("{}", e);
std::process::exit(1);
}
}
}

Some files were not shown because too many files have changed in this diff Show More