Compare commits

...

39 Commits

Author SHA1 Message Date
9242681bb2
Change button to label 2022-08-25 15:12:11 +02:00
a795200245
Improve GUI 2022-08-24 17:00:30 +02:00
8d8f4932c1
Fix clippy 2022-08-18 14:35:53 +02:00
bcf2b9e5f9
Add icons 2022-08-17 21:51:30 +02:00
2b1872ae35
Update dependencies 2022-08-17 21:03:26 +02:00
b86302534f
Read ports 2022-06-29 18:11:24 +02:00
f94ffbf276
Add fan config cli editor 2022-06-28 15:58:26 +02:00
71db69dde7
Improve build 2022-06-16 08:04:45 +02:00
f1d7f10dc9
Fix build 2022-06-15 17:55:34 +02:00
11233bedd5
Clippy suggestions 2022-06-15 16:21:20 +02:00
9c0b3baa57
Clippy suggestions 2022-06-15 15:41:13 +02:00
9e67e7f596
Upgrade dependencies 2022-06-15 15:28:07 +02:00
7fcef34f84
Fix GUI update 2022-06-15 15:20:52 +02:00
66e6fd4ac2
Update amdguid 2022-06-13 13:21:44 +02:00
24246339c1
Update amdmond 2022-06-13 13:13:22 +02:00
fcda922125
Update deps 2022-06-13 13:10:17 +02:00
0a1c07432c
Update deps 2022-06-13 13:06:51 +02:00
b2ef4cc174
cargo fmt 2022-06-13 13:00:28 +02:00
77cd6e5739
Swap axes, fix drag & drop, upgrade deps 2022-06-13 12:54:50 +02:00
04e3d14936
Merge pull request #44 from MithicSpirit/main
Fix amdmond services
2022-06-01 12:06:56 +02:00
MithicSpirit
446f3696e7
Fix amdmond services 2022-05-31 18:39:00 -04:00
7d3eed7844
Bump version 2022-04-01 16:10:27 +02:00
30f5654ff5
Merge pull request #41 from Eraden/invalid-pid-and-ensure-manual-mode
Ensure manual mode is enabled. Clean pid file at exit
2022-04-01 13:42:32 +02:00
2b95ef9bea
Ensure manual mode is enabled. Clean pid file at exit
Bump version
2022-04-01 11:22:05 +02:00
9da49210cc
Merge pull request #39 from Eraden/full-pid-check
Full pid check
2022-03-06 14:40:43 +01:00
879544adeb Fix clippy warnings 2022-02-28 18:59:19 +01:00
06aca5e3ce
Update rust docs 2022-02-27 21:58:34 +01:00
511e1863a9
Fix conflict recognition 2022-02-27 21:49:08 +01:00
592395bb48 Fix build 2022-02-26 23:11:58 +01:00
c59cef5db2 Full PID check 2022-02-26 23:05:15 +01:00
0ccda3a056 Remove exec flag 2022-02-26 23:05:15 +01:00
ae47cfd31d Documentation 2022-02-26 23:01:50 +01:00
e0fee4706f
Update README.md 2022-02-11 22:56:11 +01:00
7dc48b623f
Update README.md 2022-02-10 20:08:30 +01:00
f757c283ee
Merge pull request #35 from Eraden/gui-app-fix
Add glow, small build fixes
2022-02-10 09:52:23 +01:00
5dcfb8cc08
Add glow, small build fixes 2022-02-10 09:51:53 +01:00
14b9a694fc
Merge pull request #32 from Eraden/gui-app
GUI application
2022-02-09 19:48:01 +01:00
3fdc60191f
Remove CircleCI 2022-01-28 10:19:29 +01:00
7616ddd8fc
Start GUI
GUI in iced

Basic plot manipulation

Working solution

Fix documentation

Fix build scripts

Slice application

Change views

Very basic GUI

Reload config

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

Add save button

Xorg gui and save config to target file

Documentation and clippy fixes

Avoid compiling gui on CI

Readme files

Add missing dependencies

Add missing pgp key

Refactor workflow

Refactor workflow

Add drag and drop
2022-01-28 10:17:41 +01:00
109 changed files with 9959 additions and 337 deletions

View File

@ -10,7 +10,7 @@ env:
CARGO_TERM_COLOR: always
jobs:
build:
tests:
strategy:
fail-fast: false
matrix:
@ -19,23 +19,110 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Add key
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
- name: Add repo
env:
UBUNTU: ${{ matrix.os }}
run: echo $UBUNTU &&\
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
- name: Install binary compressor
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
- name: Add target
run: rustup target install x86_64-unknown-linux-musl
- name: Run clippy
run: cargo clippy -- -D warnings
- name: Run fmt check
run: cargo fmt -- --check
- name: Run tests
run: cargo test --verbose
clippy:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-18.04, ubuntu-20.04 ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Add key
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
- name: Add repo
env:
UBUNTU: ${{ matrix.os }}
run: echo $UBUNTU &&\
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
- name: Install binary compressor
run: sudo apt-get update && sudo apt-get install upx-ucl
- name: Build
run: cargo build --release --verbose --target=x86_64-unknown-linux-musl && strip ./target/x86_64-unknown-linux-musl/release/amdfand && upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
- name: Upload a Build Artifact
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
- name: Add target
run: rustup target install x86_64-unknown-linux-musl
- name: Run clippy
run: cargo clippy -- -D warnings
build:
needs: [clippy, tests]
strategy:
fail-fast: false
matrix:
os: [ ubuntu-18.04, ubuntu-20.04 ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Add key
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
- name: Add repo
env:
UBUNTU: ${{ matrix.os }}
run: echo $UBUNTU &&\
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
- name: Install binary compressor
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake zip
- name: Add target
run: rustup target install x86_64-unknown-linux-musl
- name: Compile
run: bash ./scripts/compile.sh
- name: Optimize
run: bash ./scripts/build.sh
- name: Collect artifacts
env:
OS: ${{ matrix.os }}
run: ./scripts/zip-ci.sh $OS
- name: Upload archive with all
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.
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

3123
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

101
README.md
View File

@ -1,24 +1,113 @@
![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)
# AMD GPU management tools
This repository holds couple tools for AMD graphic cards
* `amdfand` - fan speed daemon
* `amdvold` - voltage and overclocking tool
* `amdmond` - monitor daemon
* `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)
For more information please check README each of them.
## Roadmap
* [ ] Add support for multiple cards
* [X] Add support for multiple cards
* Multiple services must recognize card even if there's multiple same version cards is installed
* Support should be by using `--config` option
* [ ] CLI for fan config edit
* [ ] CLI for voltage edit
* [ ] GUI application using native Rust framework (ex. egui, druid)
* [X] GUI application using native Rust framework (ex. egui, druid)
## :bookmark: License
## License
This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work.
## 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
```bash
./scripts/build.sh
```
#### Download missing shared libraries
##### Check linked versions
```bash
ldd ./amdguid
```
Example output:
```
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)
```
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
```

26
amdfan/Cargo.toml Normal file
View File

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

315
amdfan/src/main.rs Normal file
View File

@ -0,0 +1,315 @@
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,6 +1,6 @@
[package]
name = "amdfand"
version = "1.0.8"
version = "1.0.13"
edition = "2018"
description = "AMDGPU fan control service"
license = "MIT OR Apache-2.0"
@ -9,16 +9,18 @@ categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["fan"] }
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
ron = { version = "0.1" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
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" }

View File

@ -19,10 +19,10 @@ Optional arguments:
## Usage
```bash
cargo install argonfand
cargo install amdfand
sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed
sudo argonfand service # check amdgpu temperature and adjust speed from config file
sudo amdfand monitor # print current temperature, current fan speed, min and max fan speed
sudo amdfand service # check amdgpu temperature and adjust speed from config file
```
## Config file

View File

@ -1,7 +1,6 @@
use gumdrop::Options;
use amdgpu::utils::hw_mons;
use amdgpu_config::fan::Config;
use gumdrop::Options;
use crate::command::Fan;
use crate::{AmdFanError, FanMode};

View File

@ -1,12 +1,11 @@
use gumdrop::Options;
use amdgpu::hw_mon::HwMon;
use amdgpu::utils::{linear_map, load_temp_inputs};
use amdgpu::{
utils, TempInput, PULSE_WIDTH_MODULATION_AUTO, PULSE_WIDTH_MODULATION_MAX,
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};
@ -90,7 +89,9 @@ impl Fan {
v.into_iter().map(|hw| Self::wrap(hw, config)).collect()
}
pub(crate) fn set_speed(&mut self, speed: f64) -> crate::Result<()> {
/// Change fan speed to given value if it's between minimal and maximal
/// value
pub fn set_speed(&mut self, speed: f64) -> crate::Result<()> {
let min = self.pwm_min() as f64;
let max = self.pwm_max() as f64;
let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u64;
@ -98,20 +99,25 @@ impl Fan {
Ok(())
}
pub(crate) fn write_manual(&self) -> crate::Result<()> {
/// Change gpu fan speed management to manual (amdfand will manage speed)
/// instead of GPU embedded manager
pub fn write_manual(&self) -> crate::Result<()> {
self.hw_mon_write(MODULATION_ENABLED_FILE, 1)
.map_err(FanError::ManualSpeedFailed)?;
Ok(())
}
pub(crate) fn write_automatic(&self) -> crate::Result<()> {
/// Change gpu fan speed management to automatic, speed will be managed by
/// GPU embedded manager
pub fn write_automatic(&self) -> crate::Result<()> {
self.hw_mon_write("pwm1_enable", 2)
.map_err(FanError::AutomaticSpeedFailed)?;
Ok(())
}
/// Change fan speed to given value with checking min-max range
fn write_pwm(&self, value: u64) -> crate::Result<()> {
if self.is_fan_automatic() {
if !self.is_fan_manual() {
self.write_manual()?;
}
self.hw_mon_write("pwm1", value)
@ -119,12 +125,16 @@ impl Fan {
Ok(())
}
pub fn is_fan_automatic(&self) -> bool {
/// 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_AUTO)
.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())?;
@ -143,7 +153,8 @@ impl Fan {
Ok(value)
}
pub(crate) fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
/// Read temperature from given input sensor
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
let value = self
.hw_mon_read(name)?
.parse::<u64>()
@ -151,13 +162,15 @@ impl Fan {
Ok(value)
}
/// Read minimal fan speed. Usually this is 0
pub fn pwm_min(&mut self) -> u32 {
if self.pwm_min.is_none() {
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
};
self.pwm_min.unwrap_or_default()
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));

View File

@ -1,9 +1,9 @@
extern crate log;
use gumdrop::Options;
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;
@ -29,10 +29,16 @@ pub struct Opts {
version: bool,
#[options(help = "Config location")]
config: Option<String>,
#[options(
help = "Pid file name (exp. card1). This should not be path, only file name without extension"
)]
pid_file: Option<String>,
#[options(command)]
command: Option<command::FanCommand>,
}
static DEFAULT_PID_FILE_NAME: &str = "amdfand";
fn run(config: Config) -> Result<()> {
let opts: Opts = Opts::parse_args_default_or_exit();
@ -46,8 +52,8 @@ fn run(config: Config) -> Result<()> {
}
match opts.command {
None => service::run(config),
Some(FanCommand::Service(_)) => service::run(config),
None => run_service(config, opts),
Some(FanCommand::Service(_)) => run_service(config, opts),
Some(FanCommand::SetAutomatic(switcher)) => {
change_mode::run(switcher, FanMode::Automatic, config)
}
@ -68,6 +74,18 @@ fn run(config: Config) -> Result<()> {
}
}
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");

View File

@ -1,20 +1,39 @@
use gumdrop::Options;
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(config: Config) -> crate::Result<()> {
/// 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()

View File

@ -1,6 +1,6 @@
[package]
name = "amdgpu-config"
version = "1.0.8"
version = "1.0.10"
edition = "2021"
description = "Subcomponent of AMDGPU tools"
license = "MIT OR Apache-2.0"
@ -16,19 +16,20 @@ path = "./src/lib.rs"
fan = []
voltage = []
monitor = []
gui = []
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
csv = { version = "1.1.6" }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
csv = { version = "1.1" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
thiserror = "1.0"
gumdrop = { version = "0.8" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] }

View File

@ -3,20 +3,38 @@ use amdgpu::{LogLevel, TempInput};
pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml";
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Default, PartialEq)]
pub struct MatrixPoint {
pub temp: f64,
pub speed: f64,
}
impl MatrixPoint {
pub const MIN: MatrixPoint = MatrixPoint {
temp: 0.0,
speed: 0.0,
};
pub const MAX: MatrixPoint = MatrixPoint {
temp: 100.0,
speed: 100.0,
};
pub fn new(temp: f64, speed: f64) -> Self {
Self { temp, speed }
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Config {
cards: Option<Vec<String>>,
log_level: LogLevel,
speed_matrix: Vec<MatrixPoint>,
/// One of temperature inputs /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input
#[serde(skip)]
path: String,
/// One of temperature inputs
/// /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input
/// If nothing is provided higher reading will be taken (this is not good!)
temp_input: Option<TempInput>,
log_level: LogLevel,
cards: Option<Vec<String>>,
speed_matrix: Vec<MatrixPoint>,
}
impl Config {
@ -28,6 +46,23 @@ impl Config {
self.cards.as_ref()
}
pub fn reload(self) -> Result<Config, ConfigError> {
let config = load_config(&self.path)?;
Ok(config)
}
pub fn speed_matrix(&self) -> &[MatrixPoint] {
&self.speed_matrix
}
pub fn speed_matrix_mut(&mut self) -> &mut [MatrixPoint] {
&mut self.speed_matrix
}
pub fn speed_matrix_vec_mut(&mut self) -> &mut Vec<MatrixPoint> {
&mut self.speed_matrix
}
pub fn speed_matrix_point(&self, temp: f64) -> Option<&MatrixPoint> {
match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
Some(idx) => self.speed_matrix.get(idx),
@ -62,6 +97,10 @@ impl Config {
self.temp_input.as_ref()
}
pub fn path(&self) -> &str {
&self.path
}
fn min_speed(&self) -> f64 {
self.speed_matrix.first().map(|p| p.speed).unwrap_or(0f64)
}
@ -74,6 +113,7 @@ impl Config {
impl Default for Config {
fn default() -> Self {
Self {
path: String::from(DEFAULT_FAN_CONFIG_PATH),
#[allow(deprecated)]
cards: None,
log_level: LogLevel::Error,
@ -143,7 +183,8 @@ pub enum ConfigError {
}
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let config = ensure_config::<Config, ConfigError, _>(config_path)?;
let mut config = ensure_config::<Config, ConfigError, _>(config_path)?;
config.path = String::from(config_path);
let mut last_point: Option<&MatrixPoint> = None;
@ -202,11 +243,10 @@ pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
#[cfg(test)]
mod parse_config {
use amdgpu::{AmdGpuError, Card, TempInput};
use serde::Deserialize;
use amdgpu::{AmdGpuError, Card, TempInput};
#[derive(Deserialize, PartialEq, Debug)]
#[derive(Deserialize, PartialEq, Eq, Debug)]
pub struct Foo {
card: Card,
}
@ -314,3 +354,20 @@ mod speed_for_temp {
assert_eq!(config.speed_for_temp(160f64), 100f64);
}
}
#[cfg(test)]
mod serde_tests {
use crate::fan::Config;
#[test]
fn serialize() {
let res = toml::to_string(&Config::default());
assert!(res.is_ok());
}
#[test]
fn deserialize() {
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
assert!(res.is_ok());
}
}

51
amdgpu-config/src/gui.rs Normal file
View File

@ -0,0 +1,51 @@
use amdgpu::utils::ensure_config;
use amdgpu::LogLevel;
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("{0}")]
Io(#[from] std::io::Error),
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Config {
/// Minimal log level
log_level: LogLevel,
}
impl Default for Config {
fn default() -> Self {
Self {
log_level: LogLevel::Error,
}
}
}
impl Config {
pub fn log_level(&self) -> LogLevel {
self.log_level
}
}
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
let config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
Ok(config)
}
#[cfg(test)]
mod serde_tests {
use crate::gui::Config;
#[test]
fn serialize() {
let res = toml::to_string(&Config::default());
assert!(res.is_ok());
}
#[test]
fn deserialize() {
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
assert!(res.is_ok());
}
}

View File

@ -1,5 +1,7 @@
#[cfg(feature = "fan")]
pub mod fan;
#[cfg(feature = "gui")]
pub mod gui;
#[cfg(feature = "monitor")]
pub mod monitor;
#[cfg(feature = "voltage")]

View File

@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize};
use amdgpu::utils::ensure_config;
use amdgpu::LogLevel;
use serde::{Deserialize, Serialize};
pub static DEFAULT_MONITOR_CONFIG_PATH: &str = "/etc/amdfand/monitor.toml";
@ -51,3 +50,20 @@ pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
Ok(config)
}
#[cfg(test)]
mod serde_tests {
use crate::monitor::Config;
#[test]
fn serialize() {
let res = toml::to_string(&Config::default());
assert!(res.is_ok());
}
#[test]
fn deserialize() {
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
assert!(res.is_ok());
}
}

View File

@ -29,3 +29,20 @@ impl Config {
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
ensure_config::<Config, ConfigError, _>(config_path)
}
#[cfg(test)]
mod serde_tests {
use crate::voltage::Config;
#[test]
fn serialize() {
let res = toml::to_string(&Config::default());
assert!(res.is_ok());
}
#[test]
fn deserialize() {
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
assert!(res.is_ok());
}
}

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "amdgpu"
version = "1.0.8"
version = "1.0.11"
edition = "2018"
description = "Subcomponent of AMDGPU fan control service"
license = "MIT OR Apache-2.0"
@ -8,11 +8,20 @@ keywords = ["hardware", "amdgpu"]
categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[dependencies]
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
[features]
gui-helper = []
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
[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

@ -2,7 +2,7 @@ use serde::Deserialize;
use crate::AmdGpuError;
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Card(pub u32);
impl std::fmt::Display for Card {

View File

@ -1,3 +1,34 @@
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`.")]
@ -10,10 +41,36 @@ pub enum AmdGpuError {
InvalidTempInput(String),
#[error("Unable to read GPU vendor")]
FailedReadVendor,
#[error("{0:?}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Io(#[from] IoFailure),
#[error("Card does not have hwmon")]
NoAmdHwMon,
#[error("{0:?}")]
PidFile(#[from] PidLockError),
#[cfg(feature = "gui-helper")]
#[error("{0:?}")]
GuiHelper(#[from] GuiHelperError),
#[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 {
@ -26,7 +83,7 @@ impl PartialEq for AmdGpuError {
(InvalidTempInput(a), InvalidTempInput(b)) => a == b,
(FailedReadVendor, FailedReadVendor) => true,
(NoAmdHwMon, NoAmdHwMon) => true,
(Io(a), Io(b)) => a.kind() == b.kind(),
(Io(a), Io(b)) => a.io.kind() == b.io.kind(),
_ => false,
}
}

View File

@ -1,4 +1,4 @@
use crate::{utils, AmdGpuError, Card, ROOT_DIR};
use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR};
#[derive(Debug)]
pub struct HwMonName(pub String);
@ -100,7 +100,10 @@ fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf {
pub fn open_hw_mon(card: Card) -> crate::Result<HwMon> {
let read_path = hw_mon_dirs_path(&card);
let entries = std::fs::read_dir(read_path)?;
let entries = std::fs::read_dir(&read_path).map_err(|io| IoFailure {
io,
path: read_path,
})?;
let name = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {

View File

@ -1,12 +1,14 @@
use serde::{Deserialize, Serialize};
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;
@ -24,12 +26,47 @@ 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)
/// 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)]

157
amdgpu/src/lock_file.rs Normal file
View File

@ -0,0 +1,157 @@
//! 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

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

133
amdgpu/src/pidfile/mod.rs Normal file
View File

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

273
amdgpu/src/pidfile/ports.rs Normal file
View File

@ -0,0 +1,273 @@
//! 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,6 +1,8 @@
use serde::Serializer;
use crate::AmdGpuError;
#[derive(PartialEq, Debug, Copy, Clone, serde::Serialize)]
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub struct TempInput(pub u16);
impl TempInput {
@ -35,6 +37,15 @@ impl std::str::FromStr for TempInput {
}
}
impl serde::Serialize for TempInput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.as_string())
}
}
impl<'de> serde::Deserialize<'de> for TempInput {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where

View File

@ -62,11 +62,10 @@ pub fn read_cards() -> std::io::Result<Vec<Card>> {
pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
Ok(read_cards()?
.into_iter()
.map(|card| {
.flat_map(|card| {
log::info!("opening hw mon for {:?}", card);
hw_mon::open_hw_mon(card)
})
.flatten()
.filter(|hw_mon| {
!filter || {
log::info!("is vendor ok? {}", hw_mon.is_amd());
@ -82,6 +81,8 @@ pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
.collect())
}
/// Try to read from config file or create new config file.
/// Create only if it does not exists, malformed file will raise error
pub fn ensure_config<Config, Error, P>(config_path: P) -> std::result::Result<Config, Error>
where
Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized,
@ -102,6 +103,7 @@ where
}
}
/// Scan sysfs for sensor files
pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
let dir = match std::fs::read_dir(hw_mon.mon_dir()) {
Ok(d) => d,
@ -117,6 +119,7 @@ pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
.collect()
}
/// Create config directory if does not exists
pub fn ensure_config_dir() -> std::io::Result<()> {
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) {
std::fs::create_dir_all(CONFIG_DIR)?;

View File

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

View File

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

33
amdgui-helper/Cargo.toml Normal file
View File

@ -0,0 +1,33 @@
[package]
name = "amdgui-helper"
version = "1.0.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" }

7
amdgui-helper/README.md Normal file
View File

@ -0,0 +1,7 @@
# amdgui-helper
Daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files
You can communicate with it using sock file `/tmp/amdgui-helper.sock` using helper `Command` from `amdgpu`.
Each connection is single use and will be terminated after sending `Response`.

127
amdgui-helper/src/main.rs Normal file
View File

@ -0,0 +1,127 @@
//! 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![]
}
}

7
amdguid/.cargo/config Normal file
View File

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

View File

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

73
amdguid/Cargo.toml Normal file
View File

@ -0,0 +1,73 @@
[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" }

15
amdguid/README.md Normal file
View File

@ -0,0 +1,15 @@
# AMD GPU gui tool
Provides basic FAN configuration.
## Roadmap
* amdvold config manipulation
* Fix Drag & drop functionality - mouse is not followed properly
* Program profiles
## Screenshots
![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png)
![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png)
![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

317
amdguid/src/app.rs Normal file
View File

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

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

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

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

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

147
amdguid/src/items.rs Normal file
View File

@ -0,0 +1,147 @@
//! Contains items that can be added to a plot.
use std::ops::RangeInclusive;
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))
}

100
amdguid/src/items/arrows.rs Normal file
View File

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

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

132
amdguid/src/items/line.rs Normal file
View File

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

@ -0,0 +1,33 @@
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MarkerShape {
Circle,
Diamond,
Square,
Cross,
Plus,
Up,
Down,
Left,
Right,
Asterisk,
}
impl MarkerShape {
/// Get a vector containing all marker shapes.
pub fn all() -> impl Iterator<Item = MarkerShape> {
[
Self::Circle,
Self::Diamond,
Self::Square,
Self::Cross,
Self::Plus,
Self::Up,
Self::Down,
Self::Left,
Self::Right,
Self::Asterisk,
]
.iter()
.copied()
}
}

View File

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

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

250
amdguid/src/items/points.rs Normal file
View File

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

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

131
amdguid/src/items/text.rs Normal file
View File

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

127
amdguid/src/items/v_line.rs Normal file
View File

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

@ -0,0 +1,22 @@
/// A value in the value-space of the plot.
///
/// Uses f64 for improved accuracy to enable plotting
/// large values (e.g. unix time on x axis).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Value {
/// This is often something monotonically increasing, such as time, but
/// doesn't have to be. Goes from left to right.
pub x: f64,
/// Goes from bottom to top (inverse of everything else in egui!).
pub y: f64,
}
impl Value {
#[inline(always)]
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
Self {
x: x.into(),
y: y.into(),
}
}
}

139
amdguid/src/items/values.rs Normal file
View File

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

44
amdguid/src/main.rs Normal file
View File

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

244
amdguid/src/transform.rs Normal file
View File

@ -0,0 +1,244 @@
use std::ops::RangeInclusive;
use egui::{pos2, remap, Pos2, Rect, Vec2};
use crate::items::Value;
/// 2D bounding box of f64 precision.
/// The range of data values we show.
#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
pub struct Bounds {
pub min: [f64; 2],
pub max: [f64; 2],
}
impl Bounds {
pub const NOTHING: Self = Self {
min: [f64::INFINITY; 2],
max: [-f64::INFINITY; 2],
};
pub fn new_symmetrical(half_extent: f64) -> Self {
Self {
min: [-half_extent; 2],
max: [half_extent; 2],
}
}
pub fn is_finite(&self) -> bool {
self.min[0].is_finite()
&& self.min[1].is_finite()
&& self.max[0].is_finite()
&& self.max[1].is_finite()
}
pub fn is_valid(&self) -> bool {
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
}
pub fn width(&self) -> f64 {
self.max[0] - self.min[0]
}
pub fn height(&self) -> f64 {
self.max[1] - self.min[1]
}
pub fn extend_with(&mut self, value: &Value) {
self.extend_with_x(value.x);
self.extend_with_y(value.y);
}
/// Expand to include the given x coordinate
pub fn extend_with_x(&mut self, x: f64) {
self.min[0] = self.min[0].min(x);
self.max[0] = self.max[0].max(x);
}
/// Expand to include the given y coordinate
pub fn extend_with_y(&mut self, y: f64) {
self.min[1] = self.min[1].min(y);
self.max[1] = self.max[1].max(y);
}
pub fn expand_x(&mut self, pad: f64) {
self.min[0] -= pad;
self.max[0] += pad;
}
pub fn expand_y(&mut self, pad: f64) {
self.min[1] -= pad;
self.max[1] += pad;
}
pub fn merge(&mut self, other: &Bounds) {
self.min[0] = self.min[0].min(other.min[0]);
self.min[1] = self.min[1].min(other.min[1]);
self.max[0] = self.max[0].max(other.max[0]);
self.max[1] = self.max[1].max(other.max[1]);
}
pub fn translate_x(&mut self, delta: f64) {
self.min[0] += delta;
self.max[0] += delta;
}
pub fn translate_y(&mut self, delta: f64) {
self.min[1] += delta;
self.max[1] += delta;
}
pub fn translate(&mut self, delta: Vec2) {
self.translate_x(delta.x as f64);
self.translate_y(delta.y as f64);
}
pub fn add_relative_margin(&mut self, margin_fraction: Vec2) {
let width = self.width().max(0.0);
let height = self.height().max(0.0);
self.expand_x(margin_fraction.x as f64 * width);
self.expand_y(margin_fraction.y as f64 * height);
}
pub fn range_x(&self) -> RangeInclusive<f64> {
self.min[0]..=self.max[0]
}
pub fn make_x_symmetrical(&mut self) {
let x_abs = self.min[0].abs().max(self.max[0].abs());
self.min[0] = -x_abs;
self.max[0] = x_abs;
}
pub fn make_y_symmetrical(&mut self) {
let y_abs = self.min[1].abs().max(self.max[1].abs());
self.min[1] = -y_abs;
self.max[1] = y_abs;
}
}
/// Contains the screen rectangle and the plot bounds and provides methods to
/// transform them.
#[derive(Clone)]
pub struct ScreenTransform {
/// The screen rectangle.
frame: Rect,
/// The plot bounds.
bounds: Bounds,
/// Whether to always center the x-range of the bounds.
x_centered: bool,
/// Whether to always center the y-range of the bounds.
y_centered: bool,
}
impl ScreenTransform {
pub fn new(frame: Rect, bounds: Bounds, x_centered: bool, y_centered: bool) -> Self {
Self {
frame,
bounds,
x_centered,
y_centered,
}
}
pub fn frame(&self) -> &Rect {
&self.frame
}
pub fn bounds(&self) -> &Bounds {
&self.bounds
}
pub fn translate_bounds(&mut self, mut delta_pos: Vec2) {
if self.x_centered {
delta_pos.x = 0.;
}
if self.y_centered {
delta_pos.y = 0.;
}
delta_pos.x *= self.dvalue_dpos()[0] as f32;
delta_pos.y *= self.dvalue_dpos()[1] as f32;
self.bounds.translate(delta_pos);
}
/// Zoom by a relative factor with the given screen position as center.
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
let center = self.value_from_position(center);
let mut new_bounds = self.bounds;
new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64);
new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64);
new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64);
new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64);
if new_bounds.is_valid() {
self.bounds = new_bounds;
}
}
pub fn position_from_value(&self, value: &Value) -> Pos2 {
let x = remap(
value.x,
self.bounds.min[0]..=self.bounds.max[0],
(self.frame.left() as f64)..=(self.frame.right() as f64),
);
let y = remap(
value.y,
self.bounds.min[1]..=self.bounds.max[1],
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
);
pos2(x as f32, y as f32)
}
pub fn value_from_position(&self, pos: Pos2) -> Value {
let x = remap(
pos.x as f64,
(self.frame.left() as f64)..=(self.frame.right() as f64),
self.bounds.min[0]..=self.bounds.max[0],
);
let y = remap(
pos.y as f64,
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
self.bounds.min[1]..=self.bounds.max[1],
);
Value::new(x, y)
}
/// delta position / delta value
pub fn dpos_dvalue_x(&self) -> f64 {
self.frame.width() as f64 / self.bounds.width()
}
/// delta position / delta value
pub fn dpos_dvalue_y(&self) -> f64 {
-self.frame.height() as f64 / self.bounds.height() // negated y axis!
}
/// delta position / delta value
pub fn dpos_dvalue(&self) -> [f64; 2] {
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
}
/// delta value / delta position
pub fn dvalue_dpos(&self) -> [f64; 2] {
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
}
pub fn get_aspect(&self) -> f64 {
let rw = self.frame.width() as f64;
let rh = self.frame.height() as f64;
(self.bounds.width() / rw) / (self.bounds.height() / rh)
}
pub fn set_aspect(&mut self, aspect: f64) {
let epsilon = 1e-5;
let current_aspect = self.get_aspect();
if current_aspect < aspect - epsilon {
self.bounds
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
} else if current_aspect > aspect + epsilon {
self.bounds
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
}
}
}

View File

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

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

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

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

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

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

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

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

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

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

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

29
amdmond-lib/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[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"] }

26
amdmond-lib/src/errors.rs Normal file
View File

@ -0,0 +1,26 @@
use amdgpu::utils;
use amdgpu_config::{fan, monitor};
#[derive(Debug, thiserror::Error)]
pub enum AmdMonError {
#[error("Mon AMD GPU card was found")]
NoHwMon,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
MonConfigError(#[from] monitor::ConfigError),
#[error("{0}")]
FanConfigError(#[from] fan::ConfigError),
#[error("{0}")]
AmdUtils(#[from] utils::AmdGpuError),
#[error("{0}")]
Csv(#[from] csv::Error),
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
NonIntTemp(std::num::ParseIntError),
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
NonIntPwm(std::num::ParseIntError),
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
InvalidMonitorFormat,
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
EmptyTempSet,
}

118
amdmond-lib/src/lib.rs Normal file
View File

@ -0,0 +1,118 @@
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,6 +1,6 @@
[package]
name = "amdmond"
version = "1.0.8"
version = "1.0.10"
edition = "2021"
description = "AMD GPU monitoring tool for Linux"
license = "MIT OR Apache-2.0"
@ -9,14 +9,15 @@ categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["monitor", "fan"] }
amdgpu = { path = "../amdgpu", version = "1.0.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 = "1.0.30"
thiserror = { version = "1.0.30" }
gumdrop = { version = "0.8.0" }
chrono = { version = "0.4.19", features = ["serde"] }
@ -27,3 +28,4 @@ pretty_env_logger = { version = "0.4.0" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }

View File

@ -1,8 +1,4 @@
use crate::{log_file, watch, AmdMonError};
use amdgpu::hw_mon::HwMon;
use amdgpu::utils::load_temp_inputs;
use amdgpu::{TempInput, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN};
use amdgpu_config::{fan, PULSE_WIDTH_MODULATION};
use crate::{log_file, watch};
#[derive(gumdrop::Options)]
pub enum Command {
@ -15,100 +11,3 @@ impl Default for Command {
Self::Watch(watch::Watch::default())
}
}
pub struct AmdMon {
temp_input: Option<TempInput>,
inputs: Vec<String>,
hw_mon: HwMon,
/// Minimal modulation (between 0-255)
pub pwm_min: Option<u32>,
/// Maximal modulation (between 0-255)
pub pwm_max: Option<u32>,
}
impl std::ops::Deref for AmdMon {
type Target = HwMon;
fn deref(&self) -> &Self::Target {
&self.hw_mon
}
}
impl AmdMon {
pub(crate) fn wrap_all(mons: Vec<HwMon>, config: &fan::Config) -> Vec<Self> {
mons.into_iter()
.map(|hw_mon| Self::wrap(hw_mon, config))
.collect()
}
pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self {
Self {
temp_input: config.temp_input().cloned(),
inputs: load_temp_inputs(&hw_mon),
hw_mon,
pwm_min: None,
pwm_max: None,
}
}
pub fn gpu_temp(&self) -> Vec<(String, crate::Result<f64>)> {
self.inputs
.clone()
.into_iter()
.map(|name| {
let temp = self
.read_gpu_temp(name.as_str())
.map(|temp| temp as f64 / 1000f64);
(name, temp)
})
.collect()
}
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
let value = self
.hw_mon_read(name)?
.parse::<u64>()
.map_err(AmdMonError::NonIntTemp)?;
Ok(value)
}
pub fn pwm(&self) -> crate::Result<u32> {
let value = self
.hw_mon_read(PULSE_WIDTH_MODULATION)?
.parse()
.map_err(AmdMonError::NonIntPwm)?;
Ok(value)
}
pub fn pwm_min(&mut self) -> u32 {
if self.pwm_min.is_none() {
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
};
self.pwm_min.unwrap_or_default()
}
pub fn pwm_max(&mut self) -> u32 {
if self.pwm_max.is_none() {
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
};
self.pwm_max.unwrap_or(255)
}
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
if let Some(input) = self.temp_input.as_ref() {
let value = self.read_gpu_temp(&input.as_string())?;
return Ok(value as f64 / 1000f64);
}
let mut results = Vec::with_capacity(self.inputs.len());
for name in self.inputs.iter() {
results.push(self.read_gpu_temp(name).unwrap_or(0));
}
results.sort_unstable();
let value = results
.last()
.copied()
.map(|temp| temp as f64 / 1000f64)
.ok_or(AmdMonError::EmptyTempSet)?;
Ok(value)
}
}

View File

@ -1,9 +1,9 @@
use crate::command::AmdMon;
use crate::AmdMonError;
use amdgpu::utils::hw_mons;
use amdgpu_config::fan;
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
use amdgpu_config::monitor::Config;
use amdmond_lib::errors::AmdMonError;
use amdmond_lib::AmdMon;
#[derive(gumdrop::Options)]
pub struct LogFile {
@ -24,7 +24,7 @@ struct Stat {
temperature_setting: f64,
}
pub fn run(command: LogFile, config: Config) -> crate::Result<()> {
pub fn run(command: LogFile, config: Config) -> amdmond_lib::Result<()> {
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
let duration = std::time::Duration::new(

View File

@ -2,39 +2,11 @@ mod command;
mod log_file;
mod watch;
use amdgpu::utils;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
use gumdrop::Options;
use crate::command::Command;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
use amdgpu_config::{fan, monitor};
#[derive(Debug, thiserror::Error)]
pub enum AmdMonError {
#[error("Mon AMD GPU card was found")]
NoHwMon,
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
MonConfigError(#[from] monitor::ConfigError),
#[error("{0}")]
FanConfigError(#[from] fan::ConfigError),
#[error("{0}")]
AmdUtils(#[from] utils::AmdGpuError),
#[error("{0}")]
Csv(#[from] csv::Error),
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
NonIntTemp(std::num::ParseIntError),
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
NonIntPwm(std::num::ParseIntError),
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
InvalidMonitorFormat,
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
EmptyTempSet,
}
pub type Result<T> = std::result::Result<T, AmdMonError>;
#[derive(gumdrop::Options)]
pub struct Opts {
@ -48,7 +20,7 @@ pub struct Opts {
pub command: Option<Command>,
}
fn run(config: Config) -> Result<()> {
fn run(config: Config) -> amdmond_lib::Result<()> {
let opts: Opts = Opts::parse_args_default_or_exit();
if opts.version {
@ -65,7 +37,7 @@ fn run(config: Config) -> Result<()> {
}
}
fn setup() -> Result<(String, Config)> {
fn setup() -> amdmond_lib::Result<(String, Config)> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "DEBUG");
}
@ -81,7 +53,7 @@ fn setup() -> Result<(String, Config)> {
Ok((config_path, config))
}
fn main() -> Result<()> {
fn main() -> amdmond_lib::Result<()> {
let (_config_path, config) = match setup() {
Ok(config) => config,
Err(e) => {

View File

@ -3,9 +3,8 @@ use std::str::FromStr;
use amdgpu::utils::{hw_mons, linear_map};
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
use amdgpu_config::{fan, monitor};
use crate::command::AmdMon;
use crate::AmdMonError;
use amdmond_lib::errors::AmdMonError;
use amdmond_lib::AmdMon;
#[derive(Debug)]
pub enum MonitorFormat {
@ -49,7 +48,7 @@ impl Default for Watch {
}
/// Start print cards temperature and fan speed
pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> {
pub fn run(monitor: Watch, _config: monitor::Config) -> amdmond_lib::Result<()> {
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
match monitor.format {
MonitorFormat::Short => short(fan_config),
@ -57,7 +56,7 @@ pub fn run(monitor: Watch, _config: monitor::Config) -> crate::Result<()> {
}
}
pub fn verbose(config: fan::Config) -> crate::Result<()> {
pub fn verbose(config: fan::Config) -> amdmond_lib::Result<()> {
let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config);
loop {
@ -100,7 +99,7 @@ pub fn verbose(config: fan::Config) -> crate::Result<()> {
}
}
pub fn short(config: fan::Config) -> crate::Result<()> {
pub fn short(config: fan::Config) -> amdmond_lib::Result<()> {
let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config);
loop {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);

15
amdportsd/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[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" }

156
amdportsd/src/main.rs Normal file
View File

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

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

View File

@ -1,6 +1,6 @@
[package]
name = "amdvold"
version = "1.0.8"
version = "1.0.10"
edition = "2018"
description = "AMDGPU fan control service"
license = "MIT OR Apache-2.0"
@ -9,16 +9,16 @@ categories = ["hardware-support"]
repository = "https://github.com/Eraden/amdgpud"
[dependencies]
amdgpu = { path = "../amdgpu", version = "1.0.8" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.8", features = ["voltage"] }
amdgpu = { path = "../amdgpu", version = "1.0.11" }
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["voltage"] }
serde = { version = "1.0.126", features = ["derive"] }
toml = { version = "0.5.8" }
thiserror = "1.0.30"
gumdrop = { version = "0.8.0" }
serde = { version = "1.0", features = ["derive"] }
toml = { version = "0.5" }
thiserror = { version = "1.0" }
gumdrop = { version = "0.8" }
log = { version = "0.4.14" }
pretty_env_logger = { version = "0.4.0" }
log = { version = "0.4" }
pretty_env_logger = { version = "0.4" }
[dev-dependencies]
amdgpu = { path = "../amdgpu", version = "1.0" }

View File

@ -5,7 +5,7 @@ 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)]
#[derive(Debug, PartialEq, Eq)]
pub struct Frequency {
pub value: u32,
pub unit: String,
@ -50,7 +50,7 @@ impl std::str::FromStr for Frequency {
}
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
pub struct Voltage {
pub value: u32,
pub unit: String,
@ -95,13 +95,13 @@ impl std::str::FromStr for Voltage {
}
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
pub struct CurvePoint {
pub freq: Frequency,
pub voltage: Voltage,
}
#[derive(Debug, thiserror::Error, PartialEq)]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ClockStateError {
#[error("Can't parse value. {0:?}")]
ParseValue(#[from] std::num::ParseIntError),
@ -113,7 +113,7 @@ pub enum ClockStateError {
InvalidEngineClockSection(String),
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
pub struct ClockState {
pub curve_labels: Vec<CurvePoint>,
pub engine_label_lowest: Option<Frequency>,

View File

@ -1,7 +1,6 @@
use gumdrop::Options;
use amdgpu::utils::ensure_config_dir;
use amdgpu_config::voltage::{load_config, Config};
use gumdrop::Options;
use crate::command::VoltageCommand;
use crate::error::VoltageError;

BIN
assets/config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/monitoring.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

BIN
assets/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

View File

@ -1,11 +0,0 @@
#!/usr/bin/env zsh
cargo build --release
strip target/x86_64-unknown-linux-musl/release/amdfand
strip target/x86_64-unknown-linux-musl/release/amdvold
strip target/x86_64-unknown-linux-musl/release/amdmond
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold
upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond

7
rustfmt.toml Normal file
View File

@ -0,0 +1,7 @@
imports_granularity = "Module"
group_imports = "StdExternalCrate"
reorder_modules = true
reorder_imports = true
use_field_init_shorthand = true
wrap_comments = true
edition = "2021"

38
scripts/build.sh Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env zsh
set -e +x
ROOT="$(git rev-parse --show-toplevel)"
cd ${ROOT}
rm -Rf ${ROOT}/tmp
mkdir ${ROOT}/tmp
./scripts/compile.sh
strip target/x86_64-unknown-linux-musl/release/amdfand
strip target/x86_64-unknown-linux-musl/release/amdvold
strip target/x86_64-unknown-linux-musl/release/amdmond
#upx --best --lzma target/x86_64-unknown-linux-musl/release/amdfand
#upx --best --lzma target/x86_64-unknown-linux-musl/release/amdvold
#upx --best --lzma target/x86_64-unknown-linux-musl/release/amdmond
function build_and_zip() {
feature=$1
zip_name=$2
cd ${ROOT}
cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features ${feature}
strip target/x86_64-unknown-linux-gnu/release/amdguid
#upx --best --lzma target/x86_64-unknown-linux-gnu/release/amdguid
cp ./target/x86_64-unknown-linux-gnu/release/amdguid ./tmp
cd ${ROOT}/tmp
zip ${zip_name}.zip ./amdguid
cd ${ROOT}
}
build_and_zip xorg-glium amdguid-glium
build_and_zip xorg-glow amdguid-glow
build_and_zip wayland amdguid-wayland

9
scripts/compile.sh Executable file
View File

@ -0,0 +1,9 @@
set -e +x
cd "$(git rev-parse --show-toplevel)"
cargo build --release --target x86_64-unknown-linux-musl --bin amdfand
cargo build --release --target x86_64-unknown-linux-musl --bin amdmond
cargo build --release --target x86_64-unknown-linux-musl --bin amdvold
cargo build --release --target x86_64-unknown-linux-musl --bin amdgui-helper
cargo build --release --target x86_64-unknown-linux-gnu --bin amdguid --no-default-features --features xorg-glium

View File

@ -14,5 +14,15 @@ cargo publish
cd $(git_root)/amdvold
cargo publish
cd $(git_root)/amdmond-lib
cargo publish
cd $(git_root)/amdmond
cargo publish
cd $(git_root)/amdgui-helper
cargo publish
cd $(git_root)/amdguid
cargo publish

60
scripts/zip-ci.sh Executable file
View File

@ -0,0 +1,60 @@
echo Building binaries for $1
cd "$(git rev-parse --show-toplevel)"
ROOT="$(git rev-parse --show-toplevel)"
./scripts/build.sh
function build_tag_gz() {
zip_name=$1
cd ${ROOT}
for name in $*; do
cp ${ROOT}/target/x86_64-unknown-linux-musl/release/${name} ${ROOT}/tmp
cp ${ROOT}/services/${name}.service ./tmp
cd ${ROOT}/tmp
tar -cvf ${zip_name}.tar.gz ${name}.service ${name}
cd ${ROOT}
done
cd ${ROOT}/tmp
for name in $*; do
rm ${name}.service ${name}
done
cd ${ROOT}
}
function tar_gui() {
tar_name=$1
cd ${ROOT}/tmp
unzip ${tar_name}.zip
cp ${ROOT}/target/x86_64-unknown-linux-musl/release/amdgui-helper ${ROOT}/tmp
cp ${ROOT}/services/amdgui-helper.service ${ROOT}/tmp
tar -cvf ${tar_name}.tar.gz amdgui-helper amdguid amdgui-helper.service
}
build_tag_gz amdfand
build_tag_gz amdmond
build_tag_gz amdvold
tar_gui amdguid-wayland
tar_gui amdguid-glium
tar_gui amdguid-glow
cd ${ROOT}/tmp
for f in $(ls *.tar.gz); do
md5sum $f
done
cd ${ROOT}/tmp
zip -R ${1}.zip *.tar.gz
#for file in $(ls *.tar.gz);
#do
# mv $file ${ROOT}/${1}-$file
#done

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