Compare commits
No commits in common. "main" and "add_temp_input" have entirely different histories.
main
...
add_temp_i
131
.github/workflows/rust.yml
vendored
131
.github/workflows/rust.yml
vendored
@ -10,119 +10,32 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Add key
|
|
||||||
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
|
||||||
- name: Add repo
|
|
||||||
env:
|
|
||||||
UBUNTU: ${{ matrix.os }}
|
|
||||||
run: echo $UBUNTU &&\
|
|
||||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
|
|
||||||
- name: Install binary compressor
|
|
||||||
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
|
|
||||||
- name: Add target
|
|
||||||
run: rustup target install x86_64-unknown-linux-musl
|
|
||||||
- name: Run fmt check
|
|
||||||
run: cargo fmt -- --check
|
|
||||||
- name: Run tests
|
|
||||||
run: cargo test --verbose
|
|
||||||
clippy:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Add key
|
|
||||||
run: wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
|
||||||
- name: Add repo
|
|
||||||
env:
|
|
||||||
UBUNTU: ${{ matrix.os }}
|
|
||||||
run: echo $UBUNTU &&\
|
|
||||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1;
|
|
||||||
- name: Install binary compressor
|
|
||||||
run: sudo apt-get update && sudo apt-get install upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev cmake
|
|
||||||
- name: Add target
|
|
||||||
run: rustup target install x86_64-unknown-linux-musl
|
|
||||||
- name: Run clippy
|
|
||||||
run: cargo clippy -- -D warnings
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [clippy, tests]
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-18.04, ubuntu-20.04 ]
|
os: [ubuntu-18.04, ubuntu-20.04]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Add key
|
- name: Add target
|
||||||
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
|
run: rustup target install x86_64-unknown-linux-musl
|
||||||
- name: Add repo
|
- name: Run clippy
|
||||||
env:
|
run: cargo clippy -- -D warnings
|
||||||
UBUNTU: ${{ matrix.os }}
|
- name: Run fmt check
|
||||||
run: echo $UBUNTU &&\
|
run: cargo fmt -- --check
|
||||||
[[ "${UBUNTU}" == "ubuntu-18.04" ]]&&\
|
- name: Run tests
|
||||||
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;
|
run: cargo test --verbose
|
||||||
- name: Install binary compressor
|
- 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
|
run: sudo apt-get update && sudo apt-get install upx-ucl
|
||||||
- name: Add target
|
- name: Build
|
||||||
run: rustup target install x86_64-unknown-linux-musl
|
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: Compile
|
- name: Upload a Build Artifact
|
||||||
run: bash ./scripts/compile.sh
|
uses: actions/upload-artifact@v2.2.4
|
||||||
- name: Optimize
|
with:
|
||||||
run: bash ./scripts/build.sh
|
# Artifact name
|
||||||
- name: Collect artifacts
|
name: amdfand-${{ matrix.os }}
|
||||||
env:
|
# A file, directory or wildcard pattern that describes what to upload
|
||||||
OS: ${{ matrix.os }}
|
path: ./target/x86_64-unknown-linux-musl/release/amdfand
|
||||||
run: ./scripts/zip-ci.sh $OS
|
# The desired behavior if no files are found using the provided path.
|
||||||
|
|
||||||
- name: Upload archive with all
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}.tar.gz
|
|
||||||
path: ./tmp/${{ matrix.os }}.tar.gz
|
|
||||||
- name: Upload amdfand
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdfand.tar.gz
|
|
||||||
path: ./tmp/amdfand.tar.gz
|
|
||||||
- name: Upload amdguid-glium
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdguid-glium.tar.gz
|
|
||||||
path: ./tmp/amdguid-glium.tar.gz
|
|
||||||
- name: Upload amdguid-glow
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdguid-glow.tar.gz
|
|
||||||
path: ./tmp/amdguid-glow.tar.gz
|
|
||||||
- name: Upload amdguid-wayland
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdguid-wayland.tar.gz
|
|
||||||
path: ./tmp/amdguid-wayland.tar.gz
|
|
||||||
- name: Upload amdmond
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdmond.tar.gz
|
|
||||||
path: ./tmp/amdmond.tar.gz
|
|
||||||
- name: Upload amdvold
|
|
||||||
uses: actions/upload-artifact@v2.2.4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.os }}-amdvold.tar.gz
|
|
||||||
path: ./tmp/amdvold.tar.gz
|
|
||||||
|
|
||||||
|
3252
Cargo.lock
generated
3252
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@ -1,13 +1,18 @@
|
|||||||
[workspace]
|
[package]
|
||||||
members = [
|
name = "amdfand"
|
||||||
"amdgpu",
|
version = "1.0.5"
|
||||||
"amdgpu-config",
|
edition = "2018"
|
||||||
"amdfan",
|
description = "AMDGPU fan control service"
|
||||||
"amdfand",
|
license = "MIT OR Apache-2.0"
|
||||||
"amdvold",
|
keywords = ["hardware", "amdgpu"]
|
||||||
"amdmond",
|
categories = ["hardware-support"]
|
||||||
"amdmond-lib",
|
repository = "https://github.com/Eraden/amdgpud"
|
||||||
"amdguid",
|
|
||||||
"amdgui-helper",
|
[dependencies]
|
||||||
"amdportsd",
|
serde = { version = "1.0.126", features = ["derive"] }
|
||||||
]
|
toml = { version = "0.5.8" }
|
||||||
|
|
||||||
|
gumdrop = { version = "0.8.0" }
|
||||||
|
|
||||||
|
log = { version = "0.4.14" }
|
||||||
|
pretty_env_logger = { version = "0.4.0" }
|
||||||
|
25
LICENSE.md
25
LICENSE.md
@ -2,15 +2,20 @@ MIT License
|
|||||||
|
|
||||||
Copyright (c) 2021 Adrian Woźniak
|
Copyright (c) 2021 Adrian Woźniak
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
in the Software without restriction, including without limitation the rights
|
||||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
The above copyright notice and this permission notice shall be included in all
|
||||||
Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
151
README.md
151
README.md
@ -1,113 +1,74 @@
|
|||||||
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
|
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
|
||||||
[![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=discord)](https://discord.gg/HXN2QXj3Gv)
|
|
||||||
[![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=buymeacoffee)](https://discord.gg/HXN2QXj3Gv)
|
|
||||||
|
|
||||||
[![Buy me a coffee](https://static.ita-prog.pl/buy-me-a-coffee-64x64.png) This project is created with love. Please, support me](https://www.buymeacoffee.com/eraden)
|
# AMDGPU Fan control service
|
||||||
|
|
||||||
# AMD GPU management tools
|
Available commands:
|
||||||
|
|
||||||
This repository holds couple tools for AMD graphic cards
|
* `monitor` - Print current temp and fan speed
|
||||||
|
* `service` - Set fan speed depends on GPU temperature
|
||||||
|
* `set-automatic` - Switch to GPU automatic fan speed control
|
||||||
|
* `set-manual` - Switch to GPU manual fan speed control
|
||||||
|
* `available` - Print available cards
|
||||||
|
|
||||||
* `amdfand` - fan speed daemon (MUSL)
|
#### amdfand set-automatic | set-manual [OPTIONS]
|
||||||
* `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.
|
Optional arguments:
|
||||||
|
|
||||||
## Roadmap
|
* -h, --help Help message
|
||||||
|
* -c, --card CARD GPU Card number
|
||||||
|
|
||||||
* [X] Add support for multiple cards
|
## Usage
|
||||||
* Multiple services must recognize card even if there's multiple same version cards is installed
|
|
||||||
* Support should be by using `--config` option
|
|
||||||
* [ ] CLI for fan config edit
|
|
||||||
* [ ] CLI for voltage edit
|
|
||||||
* [X] GUI application using native Rust framework (ex. egui, druid)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work.
|
|
||||||
|
|
||||||
## Supported OS
|
|
||||||
|
|
||||||
Only Linux is supported.
|
|
||||||
|
|
||||||
BSD OS may work if compiled from source, but it wasn't tested.
|
|
||||||
I also don't know how to enable all amd gpu features on BSD.
|
|
||||||
|
|
||||||
### Officially supported
|
|
||||||
|
|
||||||
* Arch Linux
|
|
||||||
* Ubuntu 18.04
|
|
||||||
* Ubuntu 20.04
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
It should be possible to run MUSL programs on any Linux distribution.
|
|
||||||
|
|
||||||
GLIBC applications depends on shared glibc libraries and version of this library MUST match.
|
|
||||||
|
|
||||||
If you have other version you may download linked version and place it in application directory.
|
|
||||||
|
|
||||||
Or you can compile it from source
|
|
||||||
|
|
||||||
#### Compile
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build.sh
|
cargo install argonfand
|
||||||
|
|
||||||
|
sudo argonfand monitor # print current temperature, current fan speed, min and max fan speed
|
||||||
|
sudo argonfand service # check amdgpu temperature and adjust speed from config file
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Download missing shared libraries
|
## Config file
|
||||||
|
|
||||||
##### Check linked versions
|
```toml
|
||||||
|
# /etc/amdfand/config.toml
|
||||||
|
log_level = "Error"
|
||||||
|
cards = ["card0"]
|
||||||
|
|
||||||
```bash
|
[[speed_matrix]]
|
||||||
ldd ./amdguid
|
temp = 4.0
|
||||||
|
speed = 4.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 30.0
|
||||||
|
speed = 33.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 45.0
|
||||||
|
speed = 50.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 60.0
|
||||||
|
speed = 66.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 65.0
|
||||||
|
speed = 69.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 70.0
|
||||||
|
speed = 75.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 75.0
|
||||||
|
speed = 89.0
|
||||||
|
|
||||||
|
[[speed_matrix]]
|
||||||
|
temp = 80.0
|
||||||
|
speed = 100.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Example output:
|
## :bookmark: License
|
||||||
|
|
||||||
```
|
This work is dual-licensed under Apache 2.0 and MIT.
|
||||||
linux-vdso.so.1 (0x00007ffd706df000)
|
You can choose between one of them if you use this work.
|
||||||
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
|
`SPDX-License-Identifier: Apache-2.0 OR MIT`
|
||||||
|
|
||||||
##### 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
|
|
||||||
```
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdfan"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
ron = { version = "0.1" }
|
|
||||||
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
tui = { version = "0.18.0", features = [] }
|
|
||||||
crossbeam = { version = "0.8.1" }
|
|
||||||
crossterm = { version = "0.23.2" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }
|
|
@ -1,315 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use amdgpu_config::fan::{MatrixPoint, DEFAULT_FAN_CONFIG_PATH};
|
|
||||||
use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode};
|
|
||||||
use crossterm::execute;
|
|
||||||
use crossterm::terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
};
|
|
||||||
use gumdrop::Options;
|
|
||||||
use tui::backend::{Backend, CrosstermBackend};
|
|
||||||
use tui::layout::*;
|
|
||||||
use tui::style::{Color, Modifier, Style};
|
|
||||||
use tui::symbols::Marker;
|
|
||||||
use tui::widgets::*;
|
|
||||||
use tui::{Frame, Terminal};
|
|
||||||
|
|
||||||
#[derive(Options)]
|
|
||||||
struct Opts {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
#[options(help = "Config file location")]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App<'a> {
|
|
||||||
config: amdgpu_config::fan::Config,
|
|
||||||
events: Vec<(&'a str, &'a str)>,
|
|
||||||
table_state: TableState,
|
|
||||||
selected_point: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
|
||||||
pub fn new(config: amdgpu_config::fan::Config) -> Self {
|
|
||||||
let mut table_state = TableState::default();
|
|
||||||
table_state.select(Some(0));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
events: vec![],
|
|
||||||
table_state,
|
|
||||||
selected_point: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rotate through the event list.
|
|
||||||
/// This only exists to simulate some kind of "progress"
|
|
||||||
fn on_tick(&mut self) {
|
|
||||||
if self.events.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let event = self.events.remove(0);
|
|
||||||
self.events.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), io::Error> {
|
|
||||||
let opts: Opts = Options::parse_args_default_or_exit();
|
|
||||||
let config_path = opts
|
|
||||||
.config
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|p| p.to_str())
|
|
||||||
.unwrap_or(DEFAULT_FAN_CONFIG_PATH);
|
|
||||||
let config = amdgpu_config::fan::load_config(config_path).unwrap();
|
|
||||||
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
let tick_rate = Duration::from_millis(250);
|
|
||||||
let app = App::new(config);
|
|
||||||
run_app(&mut terminal, app, tick_rate).unwrap();
|
|
||||||
|
|
||||||
// restore terminal
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_app<B: Backend>(
|
|
||||||
terminal: &mut Terminal<B>,
|
|
||||||
mut app: App,
|
|
||||||
tick_rate: Duration,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let mut last_tick = Instant::now();
|
|
||||||
loop {
|
|
||||||
terminal.draw(|f| ui(f, &mut app))?;
|
|
||||||
|
|
||||||
let timeout = tick_rate
|
|
||||||
.checked_sub(last_tick.elapsed())
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(0));
|
|
||||||
if event::poll(timeout)? {
|
|
||||||
if let Status::Exit = handle_event(&mut app)? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if last_tick.elapsed() >= tick_rate {
|
|
||||||
app.on_tick();
|
|
||||||
last_tick = Instant::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Status {
|
|
||||||
Continue,
|
|
||||||
Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(app: &mut App) -> io::Result<Status> {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') => return Ok(Status::Exit),
|
|
||||||
KeyCode::Char('w') => {
|
|
||||||
let s = toml::to_string_pretty(&app.config).unwrap();
|
|
||||||
std::fs::write(app.config.path(), &s)?;
|
|
||||||
}
|
|
||||||
KeyCode::Char(' ') => {
|
|
||||||
if let Some(index) = app.table_state.selected() {
|
|
||||||
app.selected_point = Some(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Down => match app.selected_point {
|
|
||||||
Some(index) => {
|
|
||||||
change_value(app, index, -1.0, y, y_mut);
|
|
||||||
}
|
|
||||||
None => match app.table_state.selected() {
|
|
||||||
Some(index) => {
|
|
||||||
if index + 1 < app.config.speed_matrix().len() {
|
|
||||||
app.table_state.select(Some(index + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
app.table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
KeyCode::Up => match app.selected_point {
|
|
||||||
Some(index) => {
|
|
||||||
change_value(app, index, 1.0, y, y_mut);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
if let Some(prev) = app
|
|
||||||
.table_state
|
|
||||||
.selected()
|
|
||||||
.and_then(|idx| idx.checked_sub(1))
|
|
||||||
{
|
|
||||||
app.table_state.select(Some(prev));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Left => {
|
|
||||||
if let Some(index) = app.selected_point {
|
|
||||||
change_value(app, index, -1.0, x, x_mut);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Right => {
|
|
||||||
if let Some(index) = app.selected_point {
|
|
||||||
change_value(app, index, 1.0, x, x_mut);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Status::Continue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
|
||||||
let main_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Percentage(90), Constraint::Length(4)].as_ref())
|
|
||||||
.split(f.size());
|
|
||||||
|
|
||||||
{
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Horizontal)
|
|
||||||
.constraints([Constraint::Length(30), Constraint::Min(100)].as_ref())
|
|
||||||
.split(main_chunks[0]);
|
|
||||||
|
|
||||||
{
|
|
||||||
let rows = app
|
|
||||||
.config
|
|
||||||
.speed_matrix()
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, point)| {
|
|
||||||
Row::new(vec![format!("{}", y(point)), format!("{}", x(point))]).style(
|
|
||||||
Style::default().fg(if Some(idx) == app.selected_point {
|
|
||||||
Color::Yellow
|
|
||||||
} else {
|
|
||||||
Color::White
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let table = Table::new(rows)
|
|
||||||
.block(Block::default())
|
|
||||||
.header(
|
|
||||||
Row::new(vec!["Speed", "Temp"])
|
|
||||||
.style(Style::default().fg(Color::Yellow))
|
|
||||||
.bottom_margin(1),
|
|
||||||
)
|
|
||||||
.widths(&[Constraint::Length(5), Constraint::Length(5)])
|
|
||||||
.style(Style::default().fg(Color::White))
|
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
|
||||||
.highlight_symbol("*")
|
|
||||||
.column_spacing(1);
|
|
||||||
f.render_stateful_widget(table, chunks[0], &mut app.table_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let points = app
|
|
||||||
.config
|
|
||||||
.speed_matrix()
|
|
||||||
.iter()
|
|
||||||
.map(|point| (x(point), y(point)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let dataset = vec![Dataset::default()
|
|
||||||
.data(&points)
|
|
||||||
.name("Temp/Speed")
|
|
||||||
.graph_type(GraphType::Scatter)
|
|
||||||
.marker(Marker::Dot)
|
|
||||||
.style(Style::default().fg(Color::White))];
|
|
||||||
let chart = Chart::new(dataset)
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.style(Style::default().fg(Color::White))
|
|
||||||
.border_style(Style::default().fg(Color::White))
|
|
||||||
.borders(Borders::all()),
|
|
||||||
)
|
|
||||||
.style(Style::default())
|
|
||||||
.x_axis(Axis::default().title("Speed").bounds([0.0, 100.0]))
|
|
||||||
.y_axis(Axis::default().title("Temp").bounds([0.0, 100.0]));
|
|
||||||
f.render_widget(chart, chunks[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Length(1),
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(main_chunks[1]);
|
|
||||||
|
|
||||||
f.render_widget(Paragraph::new("q to exit"), chunks[0]);
|
|
||||||
f.render_widget(Paragraph::new("Move up/down to choose setting"), chunks[1]);
|
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new("Press SPACE to select and up/down/left/right to change values"),
|
|
||||||
chunks[2],
|
|
||||||
);
|
|
||||||
f.render_widget(Paragraph::new("w to save"), chunks[3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn change_value<Read, Write>(app: &mut App, index: usize, change: f64, read: Read, write: Write)
|
|
||||||
where
|
|
||||||
Read: FnOnce(&MatrixPoint) -> f64 + Copy,
|
|
||||||
Write: FnOnce(&mut MatrixPoint) -> &mut f64,
|
|
||||||
{
|
|
||||||
let prev = index
|
|
||||||
.checked_sub(1)
|
|
||||||
.and_then(|i| app.config.speed_matrix().get(i))
|
|
||||||
.map(read)
|
|
||||||
.unwrap_or(0.0);
|
|
||||||
let next = app
|
|
||||||
.config
|
|
||||||
.speed_matrix()
|
|
||||||
.get(index + 1)
|
|
||||||
.map(read)
|
|
||||||
.unwrap_or(100.0);
|
|
||||||
let current = app
|
|
||||||
.config
|
|
||||||
.speed_matrix()
|
|
||||||
.get(index)
|
|
||||||
.map(read)
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if (prev..=next).contains(&(current + change)) {
|
|
||||||
*app.config
|
|
||||||
.speed_matrix_mut()
|
|
||||||
.get_mut(index)
|
|
||||||
.map(write)
|
|
||||||
.unwrap() += change;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn y_mut(p: &mut MatrixPoint) -> &mut f64 {
|
|
||||||
&mut p.speed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn x_mut(p: &mut MatrixPoint) -> &mut f64 {
|
|
||||||
&mut p.temp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn y(p: &MatrixPoint) -> f64 {
|
|
||||||
p.speed
|
|
||||||
}
|
|
||||||
|
|
||||||
fn x(p: &MatrixPoint) -> f64 {
|
|
||||||
p.temp
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-musl"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,27 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdfand"
|
|
||||||
version = "1.0.13"
|
|
||||||
edition = "2018"
|
|
||||||
description = "AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
ron = { version = "0.1" }
|
|
||||||
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] }
|
|
@ -1,66 +0,0 @@
|
|||||||
![GitHub](https://img.shields.io/github/license/Eraden/amdgpud)
|
|
||||||
|
|
||||||
# AMDGPU Fan control service
|
|
||||||
|
|
||||||
Available commands:
|
|
||||||
|
|
||||||
* `service` - Set fan speed depends on GPU temperature
|
|
||||||
* `set-automatic` - Switch to GPU automatic fan speed control
|
|
||||||
* `set-manual` - Switch to GPU manual fan speed control
|
|
||||||
* `available` - Print available cards
|
|
||||||
|
|
||||||
#### amdfand set-automatic | set-manual [OPTIONS]
|
|
||||||
|
|
||||||
Optional arguments:
|
|
||||||
|
|
||||||
* -h, --help Help message
|
|
||||||
* -c, --card CARD GPU Card number
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install amdfand
|
|
||||||
|
|
||||||
sudo amdfand monitor # print current temperature, current fan speed, min and max fan speed
|
|
||||||
sudo amdfand service # check amdgpu temperature and adjust speed from config file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Config file
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# /etc/amdfand/config.toml
|
|
||||||
log_level = "Error"
|
|
||||||
temp_input = "temp1_input"
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 4.0
|
|
||||||
speed = 4.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 30.0
|
|
||||||
speed = 33.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 45.0
|
|
||||||
speed = 50.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 60.0
|
|
||||||
speed = 66.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 65.0
|
|
||||||
speed = 69.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 70.0
|
|
||||||
speed = 75.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 75.0
|
|
||||||
speed = 89.0
|
|
||||||
|
|
||||||
[[speed_matrix]]
|
|
||||||
temp = 80.0
|
|
||||||
speed = 100.0
|
|
||||||
```
|
|
@ -1,180 +0,0 @@
|
|||||||
use amdgpu::hw_mon::HwMon;
|
|
||||||
use amdgpu::utils::{linear_map, load_temp_inputs};
|
|
||||||
use amdgpu::{
|
|
||||||
utils, TempInput, PULSE_WIDTH_MODULATION_MANUAL, PULSE_WIDTH_MODULATION_MAX,
|
|
||||||
PULSE_WIDTH_MODULATION_MIN, PULSE_WIDTH_MODULATION_MODE,
|
|
||||||
};
|
|
||||||
use amdgpu_config::fan::Config;
|
|
||||||
use gumdrop::Options;
|
|
||||||
|
|
||||||
use crate::{change_mode, service};
|
|
||||||
|
|
||||||
#[derive(Debug, Options)]
|
|
||||||
pub struct AvailableCards {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Options)]
|
|
||||||
pub enum FanCommand {
|
|
||||||
#[options(help = "Check AMD GPU temperature and change fan speed depends on configuration")]
|
|
||||||
Service(service::Service),
|
|
||||||
#[options(help = "Switch GPU to automatic fan speed control")]
|
|
||||||
SetAutomatic(change_mode::Switcher),
|
|
||||||
#[options(help = "Switch GPU to manual fan speed control")]
|
|
||||||
SetManual(change_mode::Switcher),
|
|
||||||
#[options(help = "Print available cards")]
|
|
||||||
Available(AvailableCards),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum FanError {
|
|
||||||
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
|
|
||||||
NonIntPwm(std::num::ParseIntError),
|
|
||||||
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
|
|
||||||
NonIntTemp(std::num::ParseIntError),
|
|
||||||
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
|
|
||||||
EmptyTempSet,
|
|
||||||
#[error("Unable to change fan speed to manual mode. {0}")]
|
|
||||||
ManualSpeedFailed(utils::AmdGpuError),
|
|
||||||
#[error("Unable to change fan speed to automatic mode. {0}")]
|
|
||||||
AutomaticSpeedFailed(utils::AmdGpuError),
|
|
||||||
#[error("Unable to change AMD GPU modulation (a.k.a. speed) to {value}. {error}")]
|
|
||||||
FailedToChangeSpeed {
|
|
||||||
value: u64,
|
|
||||||
error: utils::AmdGpuError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Fan {
|
|
||||||
pub hw_mon: HwMon,
|
|
||||||
/// List of available temperature inputs for current HW MOD
|
|
||||||
pub temp_inputs: Vec<String>,
|
|
||||||
/// Preferred temperature input
|
|
||||||
pub temp_input: Option<TempInput>,
|
|
||||||
/// Minimal modulation (between 0-255)
|
|
||||||
pub pwm_min: Option<u32>,
|
|
||||||
/// Maximal modulation (between 0-255)
|
|
||||||
pub pwm_max: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Fan {
|
|
||||||
type Target = HwMon;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.hw_mon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for Fan {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.hw_mon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODULATION_ENABLED_FILE: &str = "pwm1_enable";
|
|
||||||
|
|
||||||
impl Fan {
|
|
||||||
pub fn wrap(hw_mon: HwMon, config: &Config) -> Self {
|
|
||||||
Self {
|
|
||||||
temp_input: config.temp_input().copied(),
|
|
||||||
temp_inputs: load_temp_inputs(&hw_mon),
|
|
||||||
hw_mon,
|
|
||||||
pwm_min: None,
|
|
||||||
pwm_max: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wrap_all(v: Vec<HwMon>, config: &Config) -> Vec<Fan> {
|
|
||||||
v.into_iter().map(|hw| Self::wrap(hw, config)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change fan speed to given value if it's between minimal and maximal
|
|
||||||
/// value
|
|
||||||
pub fn set_speed(&mut self, speed: f64) -> crate::Result<()> {
|
|
||||||
let min = self.pwm_min() as f64;
|
|
||||||
let max = self.pwm_max() as f64;
|
|
||||||
let pwm = linear_map(speed, 0f64, 100f64, min, max).round() as u64;
|
|
||||||
self.write_pwm(pwm)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change gpu fan speed management to manual (amdfand will manage speed)
|
|
||||||
/// instead of GPU embedded manager
|
|
||||||
pub fn write_manual(&self) -> crate::Result<()> {
|
|
||||||
self.hw_mon_write(MODULATION_ENABLED_FILE, 1)
|
|
||||||
.map_err(FanError::ManualSpeedFailed)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change gpu fan speed management to automatic, speed will be managed by
|
|
||||||
/// GPU embedded manager
|
|
||||||
pub fn write_automatic(&self) -> crate::Result<()> {
|
|
||||||
self.hw_mon_write("pwm1_enable", 2)
|
|
||||||
.map_err(FanError::AutomaticSpeedFailed)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Change fan speed to given value with checking min-max range
|
|
||||||
fn write_pwm(&self, value: u64) -> crate::Result<()> {
|
|
||||||
if !self.is_fan_manual() {
|
|
||||||
self.write_manual()?;
|
|
||||||
}
|
|
||||||
self.hw_mon_write("pwm1", value)
|
|
||||||
.map_err(|error| FanError::FailedToChangeSpeed { value, error })?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if gpu fan is managed by GPU embedded manager
|
|
||||||
pub fn is_fan_manual(&self) -> bool {
|
|
||||||
self.hw_mon_read(PULSE_WIDTH_MODULATION_MODE)
|
|
||||||
.map(|s| s.as_str() == PULSE_WIDTH_MODULATION_MANUAL)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get maximal GPU temperature from all inputs.
|
|
||||||
/// This is not recommended since GPU can heat differently in different
|
|
||||||
/// parts and usually only temp1 should be taken for consideration.
|
|
||||||
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
|
|
||||||
if let Some(input) = self.temp_input.as_ref() {
|
|
||||||
let value = self.read_gpu_temp(&input.as_string())?;
|
|
||||||
return Ok(value as f64 / 1000f64);
|
|
||||||
}
|
|
||||||
let mut results = Vec::with_capacity(self.temp_inputs.len());
|
|
||||||
for name in self.temp_inputs.iter() {
|
|
||||||
results.push(self.read_gpu_temp(name).unwrap_or(0));
|
|
||||||
}
|
|
||||||
results.sort_unstable();
|
|
||||||
let value = results
|
|
||||||
.last()
|
|
||||||
.copied()
|
|
||||||
.map(|temp| temp as f64 / 1000f64)
|
|
||||||
.ok_or(FanError::EmptyTempSet)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read temperature from given input sensor
|
|
||||||
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
|
||||||
let value = self
|
|
||||||
.hw_mon_read(name)?
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(FanError::NonIntTemp)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read minimal fan speed. Usually this is 0
|
|
||||||
pub fn pwm_min(&mut self) -> u32 {
|
|
||||||
if self.pwm_min.is_none() {
|
|
||||||
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
|
|
||||||
};
|
|
||||||
self.pwm_min.unwrap_or(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read maximal fan speed. Usually this is 255
|
|
||||||
pub fn pwm_max(&mut self) -> u32 {
|
|
||||||
if self.pwm_max.is_none() {
|
|
||||||
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
|
|
||||||
};
|
|
||||||
self.pwm_max.unwrap_or(255)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
use amdgpu::{utils, AmdGpuError};
|
|
||||||
use amdgpu_config::fan::ConfigError;
|
|
||||||
|
|
||||||
use crate::command::FanError;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AmdFanError {
|
|
||||||
#[error("Vendor is not AMD")]
|
|
||||||
NotAmdCard,
|
|
||||||
#[error("No hwmod has been found in sysfs")]
|
|
||||||
NoHwMonFound,
|
|
||||||
#[error("No AMD Card has been found in sysfs")]
|
|
||||||
NoAmdCardFound,
|
|
||||||
#[error("{0}")]
|
|
||||||
AmdGpu(#[from] AmdGpuError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Fan(#[from] FanError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Config(#[from] ConfigError),
|
|
||||||
#[error("{0:}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("{0:}")]
|
|
||||||
AmdUtils(#[from] utils::AmdGpuError),
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
extern crate log;
|
|
||||||
|
|
||||||
use amdgpu::lock_file::PidLock;
|
|
||||||
use amdgpu::utils::{ensure_config_dir, hw_mons};
|
|
||||||
use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH};
|
|
||||||
use gumdrop::Options;
|
|
||||||
|
|
||||||
use crate::command::FanCommand;
|
|
||||||
use crate::error::AmdFanError;
|
|
||||||
|
|
||||||
mod change_mode;
|
|
||||||
mod command;
|
|
||||||
mod error;
|
|
||||||
mod panic_handler;
|
|
||||||
mod service;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AmdFanError>;
|
|
||||||
|
|
||||||
pub enum FanMode {
|
|
||||||
Manual,
|
|
||||||
Automatic,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Options)]
|
|
||||||
pub struct Opts {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
#[options(help = "Print version")]
|
|
||||||
version: bool,
|
|
||||||
#[options(help = "Config location")]
|
|
||||||
config: Option<String>,
|
|
||||||
#[options(
|
|
||||||
help = "Pid file name (exp. card1). This should not be path, only file name without extension"
|
|
||||||
)]
|
|
||||||
pid_file: Option<String>,
|
|
||||||
#[options(command)]
|
|
||||||
command: Option<command::FanCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static DEFAULT_PID_FILE_NAME: &str = "amdfand";
|
|
||||||
|
|
||||||
fn run(config: Config) -> Result<()> {
|
|
||||||
let opts: Opts = Opts::parse_args_default_or_exit();
|
|
||||||
|
|
||||||
if opts.version {
|
|
||||||
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
#[allow(deprecated)]
|
|
||||||
if config.cards().is_some() {
|
|
||||||
log::warn!("cards config field is no longer supported");
|
|
||||||
}
|
|
||||||
|
|
||||||
match opts.command {
|
|
||||||
None => run_service(config, opts),
|
|
||||||
Some(FanCommand::Service(_)) => run_service(config, opts),
|
|
||||||
Some(FanCommand::SetAutomatic(switcher)) => {
|
|
||||||
change_mode::run(switcher, FanMode::Automatic, config)
|
|
||||||
}
|
|
||||||
Some(FanCommand::SetManual(switcher)) => {
|
|
||||||
change_mode::run(switcher, FanMode::Manual, config)
|
|
||||||
}
|
|
||||||
Some(FanCommand::Available(_)) => {
|
|
||||||
println!("Available cards");
|
|
||||||
hw_mons(false)?.into_iter().for_each(|hw_mon| {
|
|
||||||
println!(
|
|
||||||
" * {:6>} - {}",
|
|
||||||
hw_mon.card(),
|
|
||||||
hw_mon.name().unwrap_or_default()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_service(config: Config, opts: Opts) -> Result<()> {
|
|
||||||
let mut pid_file = PidLock::new(
|
|
||||||
"amdfand",
|
|
||||||
opts.pid_file
|
|
||||||
.unwrap_or_else(|| String::from(DEFAULT_PID_FILE_NAME)),
|
|
||||||
)?;
|
|
||||||
pid_file.acquire()?;
|
|
||||||
let res = service::run(config);
|
|
||||||
pid_file.release()?;
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup() -> Result<(String, Config)> {
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "DEBUG");
|
|
||||||
}
|
|
||||||
pretty_env_logger::init();
|
|
||||||
ensure_config_dir()?;
|
|
||||||
|
|
||||||
let config_path = Opts::parse_args_default_or_exit()
|
|
||||||
.config
|
|
||||||
.unwrap_or_else(|| DEFAULT_FAN_CONFIG_PATH.to_string());
|
|
||||||
let config = load_config(&config_path)?;
|
|
||||||
log::info!("{:?}", config);
|
|
||||||
log::set_max_level(config.log_level().as_str().parse().unwrap());
|
|
||||||
Ok((config_path, config))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let (_config_path, config) = match setup() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match run(config) {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
panic_handler::restore_automatic();
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
use amdgpu::utils::hw_mons;
|
|
||||||
|
|
||||||
use crate::command::Fan;
|
|
||||||
|
|
||||||
pub fn restore_automatic() {
|
|
||||||
for hw in hw_mons(true).unwrap_or_default() {
|
|
||||||
if let Err(error) = (Fan {
|
|
||||||
hw_mon: hw,
|
|
||||||
temp_inputs: vec![],
|
|
||||||
temp_input: None,
|
|
||||||
pwm_min: None,
|
|
||||||
pwm_max: None,
|
|
||||||
})
|
|
||||||
.write_automatic()
|
|
||||||
{
|
|
||||||
log::error!("{}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
use amdgpu::utils::hw_mons;
|
|
||||||
use amdgpu::{config_reloaded, is_reload_required, listen_unix_signal};
|
|
||||||
use amdgpu_config::fan::Config;
|
|
||||||
use gumdrop::Options;
|
|
||||||
|
|
||||||
use crate::command::Fan;
|
|
||||||
use crate::AmdFanError;
|
|
||||||
|
|
||||||
/// Start service which will change fan speed according to config and GPU
|
|
||||||
/// temperature
|
|
||||||
pub fn run(mut config: Config) -> crate::Result<()> {
|
|
||||||
listen_unix_signal();
|
|
||||||
|
|
||||||
let mut hw_mons = Fan::wrap_all(hw_mons(true)?, &config);
|
|
||||||
|
|
||||||
if hw_mons.is_empty() {
|
|
||||||
return Err(AmdFanError::NoHwMonFound);
|
|
||||||
}
|
|
||||||
hw_mons.iter().for_each(|fan| {
|
|
||||||
if let Err(e) = fan.write_manual() {
|
|
||||||
log::debug!(
|
|
||||||
"Failed to switch to manual fan manipulation for fan {:?}. {:?}",
|
|
||||||
fan.hw_mon,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut cache = std::collections::HashMap::new();
|
|
||||||
loop {
|
|
||||||
if is_reload_required() {
|
|
||||||
log::info!("Reloading config...");
|
|
||||||
config = config.reload()?;
|
|
||||||
log::info!(" config reloaded");
|
|
||||||
config_reloaded();
|
|
||||||
}
|
|
||||||
for hw_mon in hw_mons.iter_mut() {
|
|
||||||
let gpu_temp = config
|
|
||||||
.temp_input()
|
|
||||||
.and_then(|input| {
|
|
||||||
hw_mon
|
|
||||||
.read_gpu_temp(&input.as_string())
|
|
||||||
.map(|temp| temp as f64 / 1000f64)
|
|
||||||
.ok()
|
|
||||||
})
|
|
||||||
.or_else(|| hw_mon.max_gpu_temp().ok())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
log::debug!("Current {} temperature: {}", hw_mon.card(), gpu_temp);
|
|
||||||
let last = *cache.entry(**hw_mon.card()).or_insert(1_000f64);
|
|
||||||
|
|
||||||
if (last - gpu_temp).abs() < 0.001f64 {
|
|
||||||
log::debug!("Temperature didn't change");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let speed = config.speed_for_temp(gpu_temp);
|
|
||||||
log::debug!("Resolved speed {:.2}", speed);
|
|
||||||
|
|
||||||
if let Err(e) = hw_mon.set_speed(speed) {
|
|
||||||
log::error!("Failed to change speed to {}. {:?}", speed, e);
|
|
||||||
}
|
|
||||||
cache.insert(**hw_mon.card(), gpu_temp);
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Options)]
|
|
||||||
pub struct Service {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdgpu-config"
|
|
||||||
version = "1.0.10"
|
|
||||||
edition = "2021"
|
|
||||||
description = "Subcomponent of AMDGPU tools"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "amdgpu_config"
|
|
||||||
path = "./src/lib.rs"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
fan = []
|
|
||||||
voltage = []
|
|
||||||
monitor = []
|
|
||||||
gui = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
csv = { version = "1.1" }
|
|
||||||
|
|
||||||
thiserror = "1.0"
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] }
|
|
@ -1,5 +0,0 @@
|
|||||||
# amdgpu-config
|
|
||||||
|
|
||||||
This crates holds config files for `amdfand`, `amdvold` and `amdmond`.
|
|
||||||
|
|
||||||
For more information please check those services.
|
|
@ -1,373 +0,0 @@
|
|||||||
use amdgpu::utils::{ensure_config, linear_map};
|
|
||||||
use amdgpu::{LogLevel, TempInput};
|
|
||||||
|
|
||||||
pub static DEFAULT_FAN_CONFIG_PATH: &str = "/etc/amdfand/config.toml";
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Default, PartialEq)]
|
|
||||||
pub struct MatrixPoint {
|
|
||||||
pub temp: f64,
|
|
||||||
pub speed: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MatrixPoint {
|
|
||||||
pub const MIN: MatrixPoint = MatrixPoint {
|
|
||||||
temp: 0.0,
|
|
||||||
speed: 0.0,
|
|
||||||
};
|
|
||||||
pub const MAX: MatrixPoint = MatrixPoint {
|
|
||||||
temp: 100.0,
|
|
||||||
speed: 100.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn new(temp: f64, speed: f64) -> Self {
|
|
||||||
Self { temp, speed }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
#[serde(skip)]
|
|
||||||
path: String,
|
|
||||||
/// One of temperature inputs
|
|
||||||
/// /sys/class/drm/card{X}/device/hwmon/hwmon{Y}/temp{Z}_input
|
|
||||||
/// If nothing is provided higher reading will be taken (this is not good!)
|
|
||||||
temp_input: Option<TempInput>,
|
|
||||||
log_level: LogLevel,
|
|
||||||
cards: Option<Vec<String>>,
|
|
||||||
speed_matrix: Vec<MatrixPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
#[deprecated(
|
|
||||||
since = "1.0.6",
|
|
||||||
note = "Multi-card used is halted until we will have PC with multiple AMD GPU"
|
|
||||||
)]
|
|
||||||
pub fn cards(&self) -> Option<&Vec<String>> {
|
|
||||||
self.cards.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reload(self) -> Result<Config, ConfigError> {
|
|
||||||
let config = load_config(&self.path)?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn speed_matrix(&self) -> &[MatrixPoint] {
|
|
||||||
&self.speed_matrix
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn speed_matrix_mut(&mut self) -> &mut [MatrixPoint] {
|
|
||||||
&mut self.speed_matrix
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn speed_matrix_vec_mut(&mut self) -> &mut Vec<MatrixPoint> {
|
|
||||||
&mut self.speed_matrix
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn speed_matrix_point(&self, temp: f64) -> Option<&MatrixPoint> {
|
|
||||||
match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
|
|
||||||
Some(idx) => self.speed_matrix.get(idx),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn speed_for_temp(&self, temp: f64) -> f64 {
|
|
||||||
let idx = match self.speed_matrix.iter().rposition(|p| p.temp <= temp) {
|
|
||||||
Some(idx) => idx,
|
|
||||||
_ => return self.min_speed(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if idx == self.speed_matrix.len() - 1 {
|
|
||||||
return self.max_speed();
|
|
||||||
}
|
|
||||||
|
|
||||||
linear_map(
|
|
||||||
temp,
|
|
||||||
self.speed_matrix[idx].temp,
|
|
||||||
self.speed_matrix[idx + 1].temp,
|
|
||||||
self.speed_matrix[idx].speed,
|
|
||||||
self.speed_matrix[idx + 1].speed,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log_level(&self) -> LogLevel {
|
|
||||||
self.log_level
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn temp_input(&self) -> Option<&TempInput> {
|
|
||||||
self.temp_input.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path(&self) -> &str {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn min_speed(&self) -> f64 {
|
|
||||||
self.speed_matrix.first().map(|p| p.speed).unwrap_or(0f64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn max_speed(&self) -> f64 {
|
|
||||||
self.speed_matrix.last().map(|p| p.speed).unwrap_or(100f64)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
path: String::from(DEFAULT_FAN_CONFIG_PATH),
|
|
||||||
#[allow(deprecated)]
|
|
||||||
cards: None,
|
|
||||||
log_level: LogLevel::Error,
|
|
||||||
speed_matrix: vec![
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 4f64,
|
|
||||||
speed: 4f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 30f64,
|
|
||||||
speed: 33f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 45f64,
|
|
||||||
speed: 50f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 60f64,
|
|
||||||
speed: 66f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 65f64,
|
|
||||||
speed: 69f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 70f64,
|
|
||||||
speed: 75f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 75f64,
|
|
||||||
speed: 89f64,
|
|
||||||
},
|
|
||||||
MatrixPoint {
|
|
||||||
temp: 80f64,
|
|
||||||
speed: 100f64,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
temp_input: Some(TempInput(1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("Fan speed {value:?} for config entry {index:} is too low (minimal value is 0.0)")]
|
|
||||||
FanSpeedTooLow { value: f64, index: usize },
|
|
||||||
#[error("Fan speed {value:?} for config entry {index:} is too high (maximal value is 100.0)")]
|
|
||||||
FanSpeedTooHigh { value: f64, index: usize },
|
|
||||||
#[error(
|
|
||||||
"Fan speed {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
|
|
||||||
)]
|
|
||||||
UnsortedFanSpeed {
|
|
||||||
current: f64,
|
|
||||||
index: usize,
|
|
||||||
last: f64,
|
|
||||||
},
|
|
||||||
#[error(
|
|
||||||
"Fan temperature {current:?} for config entry {index} is lower than previous value {last:?}. Entries must be sorted"
|
|
||||||
)]
|
|
||||||
UnsortedFanTemp {
|
|
||||||
current: f64,
|
|
||||||
index: usize,
|
|
||||||
last: f64,
|
|
||||||
},
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
|
||||||
let mut config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
|
||||||
config.path = String::from(config_path);
|
|
||||||
|
|
||||||
let mut last_point: Option<&MatrixPoint> = None;
|
|
||||||
|
|
||||||
for (index, matrix_point) in config.speed_matrix.iter().enumerate() {
|
|
||||||
if matrix_point.speed < 0f64 {
|
|
||||||
log::error!("Fan speed can't be below 0.0 found {}", matrix_point.speed);
|
|
||||||
return Err(ConfigError::FanSpeedTooLow {
|
|
||||||
value: matrix_point.speed,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if matrix_point.speed > 100f64 {
|
|
||||||
log::error!(
|
|
||||||
"Fan speed can't be above 100.0 found {}",
|
|
||||||
matrix_point.speed
|
|
||||||
);
|
|
||||||
return Err(ConfigError::FanSpeedTooHigh {
|
|
||||||
value: matrix_point.speed,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if let Some(last_point) = last_point {
|
|
||||||
if matrix_point.speed < last_point.speed {
|
|
||||||
log::error!(
|
|
||||||
"Curve fan speeds should be monotonically increasing, found {} then {}",
|
|
||||||
last_point.speed,
|
|
||||||
matrix_point.speed
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(ConfigError::UnsortedFanSpeed {
|
|
||||||
current: matrix_point.speed,
|
|
||||||
last: last_point.speed,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if matrix_point.temp < last_point.temp {
|
|
||||||
log::error!(
|
|
||||||
"Curve fan temps should be monotonically increasing, found {} then {}",
|
|
||||||
last_point.temp,
|
|
||||||
matrix_point.temp
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(ConfigError::UnsortedFanTemp {
|
|
||||||
current: matrix_point.temp,
|
|
||||||
last: last_point.temp,
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last_point = Some(matrix_point)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod parse_config {
|
|
||||||
use amdgpu::{AmdGpuError, Card, TempInput};
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
#[derive(Deserialize, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Foo {
|
|
||||||
card: Card,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_card0() {
|
|
||||||
assert_eq!("card0".parse::<Card>(), Ok(Card(0)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_card1() {
|
|
||||||
assert_eq!("card1".parse::<Card>(), Ok(Card(1)))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn toml_card0() {
|
|
||||||
assert_eq!(toml::from_str("card = 'card0'"), Ok(Foo { card: Card(0) }))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_invalid_temp_input() {
|
|
||||||
assert_eq!(
|
|
||||||
"".parse::<TempInput>(),
|
|
||||||
Err(AmdGpuError::InvalidTempInput("".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"12".parse::<TempInput>(),
|
|
||||||
Err(AmdGpuError::InvalidTempInput("12".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"temp12".parse::<TempInput>(),
|
|
||||||
Err(AmdGpuError::InvalidTempInput("temp12".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"12_input".parse::<TempInput>(),
|
|
||||||
Err(AmdGpuError::InvalidTempInput("12_input".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
"temp_12_input".parse::<TempInput>(),
|
|
||||||
Err(AmdGpuError::InvalidTempInput("temp_12_input".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_valid_temp_input() {
|
|
||||||
assert_eq!("temp12_input".parse::<TempInput>(), Ok(TempInput(12)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod speed_for_temp {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn below_minimal() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.speed_for_temp(1f64), 4f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn minimal() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.speed_for_temp(4f64), 4f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn between_3_and_4_temp_46() {
|
|
||||||
let config = Config::default();
|
|
||||||
// 45 -> 50
|
|
||||||
// 60 -> 66
|
|
||||||
assert_eq!(config.speed_for_temp(46f64).round(), 51f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn between_3_and_4_temp_58() {
|
|
||||||
let config = Config::default();
|
|
||||||
// 45 -> 50
|
|
||||||
// 60 -> 66
|
|
||||||
assert_eq!(config.speed_for_temp(58f64).round(), 64f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn between_3_and_4_temp_59() {
|
|
||||||
let config = Config::default();
|
|
||||||
// 45 -> 50
|
|
||||||
// 60 -> 66
|
|
||||||
assert_eq!(config.speed_for_temp(59f64).round(), 65f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn average() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.speed_for_temp(60f64), 66f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn max() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.speed_for_temp(80f64), 100f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn above_max() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.speed_for_temp(160f64), 100f64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod serde_tests {
|
|
||||||
use crate::fan::Config;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize() {
|
|
||||||
let res = toml::to_string(&Config::default());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize() {
|
|
||||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
use amdgpu::utils::ensure_config;
|
|
||||||
use amdgpu::LogLevel;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
/// Minimal log level
|
|
||||||
log_level: LogLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
log_level: LogLevel::Error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn log_level(&self) -> LogLevel {
|
|
||||||
self.log_level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
|
||||||
let config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod serde_tests {
|
|
||||||
use crate::gui::Config;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize() {
|
|
||||||
let res = toml::to_string(&Config::default());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize() {
|
|
||||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
#[cfg(feature = "fan")]
|
|
||||||
pub mod fan;
|
|
||||||
#[cfg(feature = "gui")]
|
|
||||||
pub mod gui;
|
|
||||||
#[cfg(feature = "monitor")]
|
|
||||||
pub mod monitor;
|
|
||||||
#[cfg(feature = "voltage")]
|
|
||||||
pub mod voltage;
|
|
||||||
|
|
||||||
/// pulse width modulation fan level (0-255)
|
|
||||||
pub static PULSE_WIDTH_MODULATION: &str = "pwm1";
|
|
@ -1,69 +0,0 @@
|
|||||||
use amdgpu::utils::ensure_config;
|
|
||||||
use amdgpu::LogLevel;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub static DEFAULT_MONITOR_CONFIG_PATH: &str = "/etc/amdfand/monitor.toml";
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
/// Minimal log level
|
|
||||||
log_level: LogLevel,
|
|
||||||
/// Time in milliseconds
|
|
||||||
interval: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
log_level: LogLevel::Error,
|
|
||||||
interval: 5000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn log_level(&self) -> LogLevel {
|
|
||||||
self.log_level
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn interval(&self) -> u32 {
|
|
||||||
self.interval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
|
||||||
let mut config: Config = ensure_config::<Config, ConfigError, _>(config_path)?;
|
|
||||||
|
|
||||||
if config.interval < 100 {
|
|
||||||
log::warn!(
|
|
||||||
"Minimal interval is 100ms, overriding {}ms",
|
|
||||||
config.interval
|
|
||||||
);
|
|
||||||
config.interval = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod serde_tests {
|
|
||||||
use crate::monitor::Config;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize() {
|
|
||||||
let res = toml::to_string(&Config::default());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize() {
|
|
||||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
use amdgpu::utils::ensure_config;
|
|
||||||
use amdgpu::LogLevel;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ConfigError {
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
log_level: LogLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Config {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
log_level: LogLevel::Error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn log_level(&self) -> LogLevel {
|
|
||||||
self.log_level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_config(config_path: &str) -> Result<Config, ConfigError> {
|
|
||||||
ensure_config::<Config, ConfigError, _>(config_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod serde_tests {
|
|
||||||
use crate::voltage::Config;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialize() {
|
|
||||||
let res = toml::to_string(&Config::default());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn deserialize() {
|
|
||||||
let res = toml::from_str::<Config>(&toml::to_string(&Config::default()).unwrap());
|
|
||||||
assert!(res.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-musl"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,27 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdgpu"
|
|
||||||
version = "1.0.11"
|
|
||||||
edition = "2018"
|
|
||||||
description = "Subcomponent of AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
gui-helper = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
ron = { version = "0.7" }
|
|
||||||
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
nix = { version = "0.24" }
|
|
||||||
|
|
||||||
pidlock = { version = "0.1" }
|
|
@ -1,5 +0,0 @@
|
|||||||
# amdgpu-config
|
|
||||||
|
|
||||||
This is shared data for `amdfand`, `amdvold` and `amdmond`.
|
|
||||||
|
|
||||||
For more information please check those services.
|
|
@ -1,84 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::AmdGpuError;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Card(pub u32);
|
|
||||||
|
|
||||||
impl std::fmt::Display for Card {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&format!("card{}", self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Card {
|
|
||||||
type Err = AmdGpuError;
|
|
||||||
|
|
||||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
|
||||||
if !value.starts_with("card") {
|
|
||||||
return Err(AmdGpuError::CardInvalidPrefix);
|
|
||||||
}
|
|
||||||
if value.len() < 5 {
|
|
||||||
return Err(AmdGpuError::CardInputTooShort);
|
|
||||||
}
|
|
||||||
value[4..]
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|e| AmdGpuError::CardInvalidSuffix(format!("{:?}", e)))
|
|
||||||
.map(Card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Card {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::{self, Visitor};
|
|
||||||
|
|
||||||
struct CardVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for CardVisitor {
|
|
||||||
type Value = u32;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("must have format cardX")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
match value.parse::<Card>() {
|
|
||||||
Ok(card) => Ok(*card),
|
|
||||||
Err(AmdGpuError::CardInvalidPrefix) => {
|
|
||||||
Err(E::custom(format!("expect cardX but got {}", value)))
|
|
||||||
}
|
|
||||||
Err(AmdGpuError::CardInvalidSuffix(s)) => Err(E::custom(s)),
|
|
||||||
Err(AmdGpuError::CardInputTooShort) => Err(E::custom(format!(
|
|
||||||
"{:?} must have at least 5 characters",
|
|
||||||
value
|
|
||||||
))),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deserializer.deserialize_str(CardVisitor).map(Card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl serde::Serialize for Card {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Card {
|
|
||||||
type Target = u32;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
use std::fmt::{Debug, Display, Formatter};
|
|
||||||
|
|
||||||
use pidlock::PidlockError;
|
|
||||||
|
|
||||||
use crate::lock_file::LockFileError;
|
|
||||||
#[cfg(feature = "gui-helper")]
|
|
||||||
use crate::pidfile::helper_cmd::GuiHelperError;
|
|
||||||
use crate::pidfile::ports::PortsError;
|
|
||||||
|
|
||||||
pub struct IoFailure {
|
|
||||||
pub io: std::io::Error,
|
|
||||||
pub path: std::path::PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for IoFailure {}
|
|
||||||
|
|
||||||
impl Debug for IoFailure {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!(
|
|
||||||
"File system error for {:?}. {:?}",
|
|
||||||
self.path, self.io
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for IoFailure {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_fmt(format_args!("{:?}", self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AmdGpuError {
|
|
||||||
#[error("Card must starts with `card`.")]
|
|
||||||
CardInvalidPrefix,
|
|
||||||
#[error("Card must start with `card` and ends with a number. The given name is too short.")]
|
|
||||||
CardInputTooShort,
|
|
||||||
#[error("Value after `card` is invalid {0:}")]
|
|
||||||
CardInvalidSuffix(String),
|
|
||||||
#[error("Invalid temperature input")]
|
|
||||||
InvalidTempInput(String),
|
|
||||||
#[error("Unable to read GPU vendor")]
|
|
||||||
FailedReadVendor,
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] IoFailure),
|
|
||||||
#[error("Card does not have hwmon")]
|
|
||||||
NoAmdHwMon,
|
|
||||||
#[error("{0:?}")]
|
|
||||||
PidFile(#[from] PidLockError),
|
|
||||||
#[cfg(feature = "gui-helper")]
|
|
||||||
#[error("{0:?}")]
|
|
||||||
GuiHelper(#[from] GuiHelperError),
|
|
||||||
#[error("{0:?}")]
|
|
||||||
Ports(#[from] PortsError),
|
|
||||||
#[error("{0:?}")]
|
|
||||||
LockFile(#[from] LockFileError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum PidLockError {
|
|
||||||
#[error("A lock already exists")]
|
|
||||||
LockExists,
|
|
||||||
#[error("An operation was attempted in the wrong state, e.g. releasing before acquiring.")]
|
|
||||||
InvalidState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<pidlock::PidlockError> for PidLockError {
|
|
||||||
fn from(e: PidlockError) -> Self {
|
|
||||||
match e {
|
|
||||||
pidlock::PidlockError::LockExists => PidLockError::LockExists,
|
|
||||||
pidlock::PidlockError::InvalidState => PidLockError::InvalidState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for AmdGpuError {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
use AmdGpuError::*;
|
|
||||||
match (self, other) {
|
|
||||||
(CardInvalidPrefix, CardInvalidPrefix) => true,
|
|
||||||
(CardInputTooShort, CardInputTooShort) => true,
|
|
||||||
(CardInvalidSuffix(a), CardInvalidSuffix(b)) => a == b,
|
|
||||||
(InvalidTempInput(a), InvalidTempInput(b)) => a == b,
|
|
||||||
(FailedReadVendor, FailedReadVendor) => true,
|
|
||||||
(NoAmdHwMon, NoAmdHwMon) => true,
|
|
||||||
(Io(a), Io(b)) => a.io.kind() == b.io.kind(),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct HwMonName(pub String);
|
|
||||||
|
|
||||||
impl std::ops::Deref for HwMonName {
|
|
||||||
type Target = String;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct HwMon {
|
|
||||||
/// HW MOD cord (ex. card0)
|
|
||||||
pub card: Card,
|
|
||||||
/// MW MOD name (ex. hwmod0)
|
|
||||||
pub name: HwMonName,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HwMon {
|
|
||||||
pub fn new(card: &Card, name: HwMonName) -> Self {
|
|
||||||
Self { card: *card, name }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn card(&self) -> &Card {
|
|
||||||
&self.card
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn name(&self) -> utils::Result<String> {
|
|
||||||
self.hw_mon_read("name")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn is_amd(&self) -> bool {
|
|
||||||
self.device_read("vendor")
|
|
||||||
.map_err(|_| AmdGpuError::FailedReadVendor)
|
|
||||||
.map(|vendor| vendor.trim() == "0x1002")
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn name_is_amd(&self) -> bool {
|
|
||||||
self.name().ok().filter(|s| s.trim() == "amdgpu").is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mon_file_path(&self, name: &str) -> std::path::PathBuf {
|
|
||||||
self.mon_dir().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn device_dir(&self) -> std::path::PathBuf {
|
|
||||||
std::path::PathBuf::new()
|
|
||||||
.join(ROOT_DIR)
|
|
||||||
.join(self.card.to_string())
|
|
||||||
.join("device")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mon_dir(&self) -> std::path::PathBuf {
|
|
||||||
self.device_dir().join("hwmon").join(&*self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn value_or<R: std::str::FromStr>(&self, name: &str, fallback: R) -> R {
|
|
||||||
self.hw_mon_read(name)
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
.unwrap_or(fallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hw_mon_read(&self, name: &str) -> utils::Result<String> {
|
|
||||||
utils::read_to_string(self.mon_file_path(name)).map(|s| String::from(s.trim()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn device_read(&self, name: &str) -> utils::Result<String> {
|
|
||||||
utils::read_to_string(self.device_dir().join(name)).map(|s| String::from(s.trim()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hw_mon_write(&self, name: &str, value: u64) -> utils::Result<()> {
|
|
||||||
utils::write(self.mon_file_path(name), format!("{}", value))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn device_write<C: AsRef<[u8]>>(&self, name: &str, value: C) -> utils::Result<()> {
|
|
||||||
utils::write(self.device_dir().join(name), value)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn hw_mon_dirs_path(card: &Card) -> std::path::PathBuf {
|
|
||||||
std::path::PathBuf::new()
|
|
||||||
.join(ROOT_DIR)
|
|
||||||
.join(card.to_string())
|
|
||||||
.join("device")
|
|
||||||
.join("hwmon")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open_hw_mon(card: Card) -> crate::Result<HwMon> {
|
|
||||||
let read_path = hw_mon_dirs_path(&card);
|
|
||||||
let entries = std::fs::read_dir(&read_path).map_err(|io| IoFailure {
|
|
||||||
io,
|
|
||||||
path: read_path,
|
|
||||||
})?;
|
|
||||||
let name = entries
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.filter_map(|entry| {
|
|
||||||
entry
|
|
||||||
.file_name()
|
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.filter(|name| name.starts_with("hwmon"))
|
|
||||||
.map(String::from)
|
|
||||||
.map(HwMonName)
|
|
||||||
})
|
|
||||||
.take(1)
|
|
||||||
.last()
|
|
||||||
.ok_or(AmdGpuError::NoAmdHwMon)?;
|
|
||||||
Ok(HwMon::new(&card, name))
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
pub use card::*;
|
|
||||||
pub use error::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
pub use temp_input::*;
|
|
||||||
|
|
||||||
mod card;
|
|
||||||
mod error;
|
|
||||||
#[cfg(feature = "gui-helper")]
|
|
||||||
pub mod hw_mon;
|
|
||||||
pub mod lock_file;
|
|
||||||
pub mod pidfile;
|
|
||||||
mod temp_input;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
pub static CONFIG_DIR: &str = "/etc/amdfand";
|
|
||||||
|
|
||||||
pub static ROOT_DIR: &str = "/sys/class/drm";
|
|
||||||
pub static HW_MON_DIR: &str = "hwmon";
|
|
||||||
|
|
||||||
/// pulse width modulation fan control minimum level (0)
|
|
||||||
pub static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min";
|
|
||||||
|
|
||||||
/// pulse width modulation fan control maximum level (255)
|
|
||||||
pub static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max";
|
|
||||||
|
|
||||||
/// pulse width modulation fan level (0-255)
|
|
||||||
pub static PULSE_WIDTH_MODULATION: &str = "pwm1";
|
|
||||||
|
|
||||||
/// pulse width modulation fan control method (0: no fan speed control, 1:
|
|
||||||
/// manual fan speed control using pwm interface, 2: automatic fan speed
|
|
||||||
/// control)
|
|
||||||
pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable";
|
|
||||||
|
|
||||||
// static PULSE_WIDTH_MODULATION_DISABLED: &str = "0";
|
|
||||||
pub static PULSE_WIDTH_MODULATION_MANUAL: &str = "1";
|
|
||||||
pub static PULSE_WIDTH_MODULATION_AUTO: &str = "2";
|
|
||||||
|
|
||||||
static mut RELOAD_CONFIG: bool = false;
|
|
||||||
|
|
||||||
extern "C" fn sig_reload(_n: i32) {
|
|
||||||
unsafe {
|
|
||||||
RELOAD_CONFIG = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Listen for SIGHUP signal. This signal is used to reload config
|
|
||||||
pub fn listen_unix_signal() {
|
|
||||||
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
|
|
||||||
unsafe {
|
|
||||||
let handler: SigHandler = SigHandler::Handler(sig_reload);
|
|
||||||
let action = SigAction::new(handler, SaFlags::SA_NOCLDWAIT, SigSet::empty());
|
|
||||||
sigaction(Signal::SIGHUP, &action).expect("Failed to mount action handler");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if application received SIGHUP and must reload config file
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn is_reload_required() -> bool {
|
|
||||||
unsafe { RELOAD_CONFIG }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset reload config flag
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn config_reloaded() {
|
|
||||||
unsafe {
|
|
||||||
RELOAD_CONFIG = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AmdGpuError>;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
|
||||||
pub enum LogLevel {
|
|
||||||
/// A level lower than all log levels.
|
|
||||||
Off,
|
|
||||||
/// Corresponds to the `Error` log level.
|
|
||||||
Error,
|
|
||||||
/// Corresponds to the `Warn` log level.
|
|
||||||
Warn,
|
|
||||||
/// Corresponds to the `Info` log level.
|
|
||||||
Info,
|
|
||||||
/// Corresponds to the `Debug` log level.
|
|
||||||
Debug,
|
|
||||||
/// Corresponds to the `Trace` log level.
|
|
||||||
Trace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogLevel {
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
LogLevel::Off => "OFF",
|
|
||||||
LogLevel::Error => "ERROR",
|
|
||||||
LogLevel::Warn => "WARN",
|
|
||||||
LogLevel::Info => "INFO",
|
|
||||||
LogLevel::Debug => "DEBUG",
|
|
||||||
LogLevel::Trace => "TRACE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,157 +0,0 @@
|
|||||||
//! Create lock file and prevent running 2 identical services.
|
|
||||||
//! NOTE: For 2 amdfand services you may just give 2 different names
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use nix::libc;
|
|
||||||
|
|
||||||
use crate::pidfile::Pid;
|
|
||||||
use crate::IoFailure;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum LockFileError {
|
|
||||||
#[error("Failed to read {path}. {err:?}")]
|
|
||||||
Unreadable { err: std::io::Error, path: String },
|
|
||||||
#[error("Pid {pid:?} file system error. {err:?}")]
|
|
||||||
Io { err: std::io::Error, pid: Pid },
|
|
||||||
#[error("Pid {0:?} does not exists")]
|
|
||||||
NotExists(Pid),
|
|
||||||
#[error("Pid {pid:?} with name {name:?} already exists")]
|
|
||||||
Conflict { name: String, pid: Pid },
|
|
||||||
#[error("Can't parse Pid value. {0:?}")]
|
|
||||||
MalformedPidFile(#[from] std::num::ParseIntError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum State {
|
|
||||||
NotExists,
|
|
||||||
Pending(Pid),
|
|
||||||
Dead,
|
|
||||||
New(Pid),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PidLock {
|
|
||||||
name: String,
|
|
||||||
pid_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PidLock {
|
|
||||||
pub fn new<P: AsRef<Path>>(
|
|
||||||
sub_dir: P,
|
|
||||||
name: String,
|
|
||||||
) -> std::result::Result<Self, crate::error::AmdGpuError> {
|
|
||||||
let pid_dir_path = std::path::Path::new("/var").join("lib").join(sub_dir);
|
|
||||||
let pid_path = {
|
|
||||||
std::fs::create_dir_all(&pid_dir_path).map_err(|io| IoFailure {
|
|
||||||
io,
|
|
||||||
path: pid_dir_path.clone(),
|
|
||||||
})?;
|
|
||||||
pid_dir_path
|
|
||||||
.join(format!("{}.pid", name))
|
|
||||||
.to_str()
|
|
||||||
.map(String::from)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
log::debug!("Creating pid lock for path {:?}", pid_path);
|
|
||||||
Ok(Self { pid_path, name })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create new lock file. File will be created if:
|
|
||||||
/// * pid file does not exists
|
|
||||||
/// * pid file exists but process is dead
|
|
||||||
/// * old pid and current pid have different names (lock file exists after
|
|
||||||
/// reboot and PID was taken by other process)
|
|
||||||
pub fn acquire(&mut self) -> Result<(), crate::error::AmdGpuError> {
|
|
||||||
log::debug!("PID LOCK acquiring {}", self.pid_path);
|
|
||||||
let pid = self.process_pid();
|
|
||||||
if let Some(old) = self.old_pid() {
|
|
||||||
let old = old?;
|
|
||||||
if !self.is_alive(old) {
|
|
||||||
log::debug!("Old pid {:?} is dead, overriding...", old.0);
|
|
||||||
|
|
||||||
self.enforce_pid_file(pid)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
match self.process_name(old) {
|
|
||||||
Err(LockFileError::NotExists(..)) => {
|
|
||||||
log::debug!(
|
|
||||||
"Old pid {:?} doesn't have process stat, overriding....",
|
|
||||||
old.0
|
|
||||||
);
|
|
||||||
self.enforce_pid_file(old)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::debug!("Lock error {:?}", e);
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
Ok(name) if name.ends_with(&format!("/{}", self.name)) => {
|
|
||||||
log::warn!("Conflicting {} and {} for process {}", old.0, pid.0, name);
|
|
||||||
return Err(LockFileError::Conflict { pid: old, name }.into());
|
|
||||||
}
|
|
||||||
Ok(name /* name isn't the same */) => {
|
|
||||||
log::debug!(
|
|
||||||
"Old process {:?} and current process {:?} have different names, overriding....",
|
|
||||||
name, self.name
|
|
||||||
);
|
|
||||||
self.enforce_pid_file(old)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::debug!("No collision detected");
|
|
||||||
self.enforce_pid_file(pid)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove lock file
|
|
||||||
pub fn release(&mut self) -> Result<(), crate::error::AmdGpuError> {
|
|
||||||
if let Err(e) = std::fs::remove_file(&self.pid_path) {
|
|
||||||
log::error!("Failed to release pid file {}. {:?}", self.pid_path, e);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read old pid value from file
|
|
||||||
fn old_pid(&self) -> Option<Result<Pid, LockFileError>> {
|
|
||||||
match std::fs::read_to_string(&self.pid_path) {
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
|
||||||
Err(e) => Some(Err(LockFileError::Unreadable {
|
|
||||||
path: self.pid_path.clone(),
|
|
||||||
err: e,
|
|
||||||
})),
|
|
||||||
Ok(s) => match s.parse::<i32>() {
|
|
||||||
Err(e) => Some(Err(LockFileError::MalformedPidFile(e))),
|
|
||||||
Ok(pid) => Some(Ok(Pid(pid))),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if PID is alive
|
|
||||||
fn is_alive(&self, pid: Pid) -> bool {
|
|
||||||
unsafe {
|
|
||||||
let result = libc::kill(pid.0, 0);
|
|
||||||
result == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get current process PID
|
|
||||||
fn process_pid(&self) -> Pid {
|
|
||||||
Pid(std::process::id() as i32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read target process name
|
|
||||||
fn process_name(&self, pid: Pid) -> Result<String, LockFileError> {
|
|
||||||
match std::fs::read_to_string(format!("/proc/{}/cmdline", *pid)) {
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
Err(LockFileError::NotExists(pid))
|
|
||||||
}
|
|
||||||
Err(err) => Err(LockFileError::Io { err, pid }),
|
|
||||||
Ok(s) => Ok(String::from(s.split('\0').next().unwrap_or_default())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Override pid lock file
|
|
||||||
fn enforce_pid_file(&self, pid: Pid) -> Result<(), LockFileError> {
|
|
||||||
std::fs::write(&self.pid_path, format!("{}", pid.0))
|
|
||||||
.map_err(|e| LockFileError::Io { pid, err: e })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
//! AMD GUI helper communication toolkit
|
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
|
|
||||||
use crate::pidfile::{Pid, PidResponse};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum GuiHelperError {
|
|
||||||
#[error("GUI Helper socket file not found. Is service running?")]
|
|
||||||
NoSockFile,
|
|
||||||
#[error("Failed to connect to /var/lib/amdfand/helper.sock. {0}")]
|
|
||||||
UnableToConnect(#[from] std::io::Error),
|
|
||||||
#[error("Failed to service helper command. {0}")]
|
|
||||||
Serialize(#[from] ron::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Command {
|
|
||||||
ReloadConfig { pid: Pid },
|
|
||||||
FanServices,
|
|
||||||
SaveFanConfig { path: String, content: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Response {
|
|
||||||
NoOp,
|
|
||||||
Services(Vec<Pid>),
|
|
||||||
ConfigFileSaved,
|
|
||||||
ConfigFileSaveFailed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PidResponse for Response {
|
|
||||||
fn kill_response() -> Self {
|
|
||||||
Self::NoOp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sock_file() -> std::path::PathBuf {
|
|
||||||
std::path::Path::new("/tmp").join("amdgui-helper.sock")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_command(cmd: Command) -> crate::Result<Response> {
|
|
||||||
let sock_path = sock_file();
|
|
||||||
|
|
||||||
if !sock_path.exists() {
|
|
||||||
return Err(GuiHelperError::NoSockFile.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(&sock_path).map_err(GuiHelperError::UnableToConnect)?;
|
|
||||||
let s = ron::to_string(&cmd).map_err(GuiHelperError::Serialize)?;
|
|
||||||
if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() {
|
|
||||||
log::info!("Command send");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: Response = {
|
|
||||||
let mut s = String::with_capacity(100);
|
|
||||||
let _ = stream.read_to_string(&mut s);
|
|
||||||
ron::from_str(&s).map_err(GuiHelperError::Serialize)?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
use std::fmt::Debug;
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::net::Shutdown;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
pub mod helper_cmd;
|
|
||||||
pub mod ports;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct Pid(pub i32);
|
|
||||||
|
|
||||||
impl Deref for Pid {
|
|
||||||
type Target = i32;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_connection<HandleCmd, Cmd, Res>(stream: UnixStream, handle_command: HandleCmd)
|
|
||||||
where
|
|
||||||
HandleCmd: FnOnce(Service<Res>, Cmd) + Copy,
|
|
||||||
Cmd: DeserializeOwned + Serialize + Debug,
|
|
||||||
Res: DeserializeOwned + Serialize + Debug + PidResponse,
|
|
||||||
{
|
|
||||||
let mut service = Service::<Res>::new(stream);
|
|
||||||
|
|
||||||
let command = match service.read_command() {
|
|
||||||
Some(s) => s,
|
|
||||||
_ => return service.kill(),
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!("Incoming {:?}", command);
|
|
||||||
let cmd = match ron::from_str::<Cmd>(command.trim()) {
|
|
||||||
Ok(cmd) => cmd,
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Invalid message {:?}. {:?}", command, e);
|
|
||||||
return service.kill();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handle_command(service, cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PidResponse: Sized {
|
|
||||||
fn kill_response() -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Service<Response>(UnixStream, PhantomData<Response>)
|
|
||||||
where
|
|
||||||
Response: serde::Serialize + Debug + PidResponse;
|
|
||||||
|
|
||||||
impl<Response> Service<Response>
|
|
||||||
where
|
|
||||||
Response: serde::Serialize + Debug + PidResponse,
|
|
||||||
{
|
|
||||||
pub fn new(file: UnixStream) -> Self {
|
|
||||||
Self(file, Default::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize and send command
|
|
||||||
pub fn write_response(&mut self, res: Response) {
|
|
||||||
write_response(&mut self.0, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read from `.sock` file new line separated commands
|
|
||||||
pub fn read_command(&mut self) -> Option<String> {
|
|
||||||
read_command(&mut self.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Close connection with no operation response
|
|
||||||
pub fn kill(mut self) {
|
|
||||||
self.write_response(Response::kill_response());
|
|
||||||
self.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self) {
|
|
||||||
let _ = self.0.shutdown(Shutdown::Both);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize and send command
|
|
||||||
pub fn write_response<Response>(file: &mut UnixStream, res: Response)
|
|
||||||
where
|
|
||||||
Response: serde::Serialize + Debug,
|
|
||||||
{
|
|
||||||
match ron::to_string(&res) {
|
|
||||||
Ok(buffer) => match file.write_all(buffer.as_bytes()) {
|
|
||||||
Ok(_) => {
|
|
||||||
log::info!("Response successfully written")
|
|
||||||
}
|
|
||||||
Err(e) => log::warn!("Failed to write response. {:?}", e),
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to serialize response {:?}. {:?}", res, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read from `.sock` file new line separated commands
|
|
||||||
pub fn read_command(file: &mut UnixStream) -> Option<String> {
|
|
||||||
let mut command = String::with_capacity(100);
|
|
||||||
log::info!("Reading stream...");
|
|
||||||
read_line(file, &mut command);
|
|
||||||
if command.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_line(stream: &mut UnixStream, command: &mut String) {
|
|
||||||
let mut buffer = [0];
|
|
||||||
while stream.read_exact(&mut buffer).is_ok() {
|
|
||||||
if buffer[0] == b'\n' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
match std::str::from_utf8(&buffer) {
|
|
||||||
Ok(s) => {
|
|
||||||
command.push_str(s);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to read from client. {:?}", e);
|
|
||||||
let _ = stream.shutdown(Shutdown::Both);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,273 +0,0 @@
|
|||||||
//! AMD GUI helper communication toolkit
|
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::pidfile::PidResponse;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum PortsError {
|
|
||||||
#[error("AMD GPU ports socket file not found. Is service running?")]
|
|
||||||
NoSockFile,
|
|
||||||
#[error("Failed to connect to /tmp/amdgpu-ports.sock. {0}")]
|
|
||||||
UnableToConnect(#[from] std::io::Error),
|
|
||||||
#[error("Failed to ports command. {0}")]
|
|
||||||
Serialize(#[from] ron::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
|
||||||
pub enum OutputType {
|
|
||||||
Reserved,
|
|
||||||
#[serde(rename = "v")]
|
|
||||||
Vga,
|
|
||||||
#[serde(rename = "m")]
|
|
||||||
MiniDvi,
|
|
||||||
#[serde(rename = "h")]
|
|
||||||
Hdmi,
|
|
||||||
#[serde(rename = "a")]
|
|
||||||
Audio,
|
|
||||||
#[serde(rename = "o")]
|
|
||||||
OpticalAudio,
|
|
||||||
#[serde(rename = "d")]
|
|
||||||
Dvi,
|
|
||||||
#[serde(rename = "t")]
|
|
||||||
Thunderbolt,
|
|
||||||
#[serde(rename = "D")]
|
|
||||||
DisplayPort,
|
|
||||||
#[serde(rename = "M")]
|
|
||||||
MiniDisplayPort,
|
|
||||||
#[serde(rename = "f")]
|
|
||||||
FireWire400,
|
|
||||||
#[serde(rename = "p")]
|
|
||||||
Ps2,
|
|
||||||
#[serde(rename = "s")]
|
|
||||||
Sata,
|
|
||||||
#[serde(rename = "e")]
|
|
||||||
ESata,
|
|
||||||
#[serde(rename = "E")]
|
|
||||||
Ethernet,
|
|
||||||
#[serde(rename = "F")]
|
|
||||||
FireWire800,
|
|
||||||
#[serde(rename = "1")]
|
|
||||||
UsbTypeA,
|
|
||||||
#[serde(rename = "2")]
|
|
||||||
UsbTypeB,
|
|
||||||
#[serde(rename = "3")]
|
|
||||||
UsbTypeC,
|
|
||||||
#[serde(rename = "4")]
|
|
||||||
MicroUsb,
|
|
||||||
#[serde(rename = "5")]
|
|
||||||
MimiUsb,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputType {
|
|
||||||
pub fn to_coords(&self) -> (u32, u32) {
|
|
||||||
match self {
|
|
||||||
OutputType::Reserved => (0, 0),
|
|
||||||
//
|
|
||||||
OutputType::Vga => (0, 0),
|
|
||||||
OutputType::MiniDvi => (80, 0),
|
|
||||||
OutputType::Hdmi => (160, 0),
|
|
||||||
OutputType::Audio => (240, 0),
|
|
||||||
OutputType::OpticalAudio => (320, 0),
|
|
||||||
//
|
|
||||||
OutputType::Dvi => (0, 80),
|
|
||||||
OutputType::Thunderbolt => (80, 80),
|
|
||||||
OutputType::DisplayPort => (160, 80),
|
|
||||||
OutputType::MiniDisplayPort => (240, 80),
|
|
||||||
OutputType::FireWire400 => (320, 80),
|
|
||||||
//
|
|
||||||
OutputType::Ps2 => (0, 160),
|
|
||||||
OutputType::Sata => (80, 160),
|
|
||||||
OutputType::ESata => (160, 160),
|
|
||||||
OutputType::Ethernet => (240, 160),
|
|
||||||
OutputType::FireWire800 => (320, 160),
|
|
||||||
//
|
|
||||||
OutputType::UsbTypeA => (0, 240),
|
|
||||||
OutputType::UsbTypeB => (80, 240),
|
|
||||||
OutputType::UsbTypeC => (160, 240),
|
|
||||||
OutputType::MicroUsb => (240, 240),
|
|
||||||
OutputType::MimiUsb => (320, 240),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
OutputType::Reserved => "-----",
|
|
||||||
//
|
|
||||||
OutputType::Vga => "Vga",
|
|
||||||
OutputType::MiniDvi => "MiniDvi",
|
|
||||||
OutputType::Hdmi => "Hdmi",
|
|
||||||
OutputType::Audio => "Audio",
|
|
||||||
OutputType::OpticalAudio => "OptimalAudio",
|
|
||||||
//
|
|
||||||
OutputType::Dvi => "Dvi",
|
|
||||||
OutputType::Thunderbolt => "Thunderbolt",
|
|
||||||
OutputType::DisplayPort => "DisplayPort",
|
|
||||||
OutputType::MiniDisplayPort => "MiniDisplayPort",
|
|
||||||
OutputType::FireWire400 => "FireWire400",
|
|
||||||
//
|
|
||||||
OutputType::Ps2 => "Ps2",
|
|
||||||
OutputType::Sata => "Sata",
|
|
||||||
OutputType::ESata => "ESata",
|
|
||||||
OutputType::Ethernet => "Ethernet",
|
|
||||||
OutputType::FireWire800 => "FireWire800",
|
|
||||||
//
|
|
||||||
OutputType::UsbTypeA => "UsbTypeA",
|
|
||||||
OutputType::UsbTypeB => "UsbTypeB",
|
|
||||||
OutputType::UsbTypeC => "UsbTypeC",
|
|
||||||
OutputType::MicroUsb => "MicroUsb",
|
|
||||||
OutputType::MimiUsb => "MimiUsb",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_str(s: &str) -> Option<Self> {
|
|
||||||
Some(match s {
|
|
||||||
"DP" => Self::DisplayPort,
|
|
||||||
"eDP" => Self::MiniDisplayPort,
|
|
||||||
"DVI" => Self::Dvi,
|
|
||||||
"HDMI" => Self::Hdmi,
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all() -> [OutputType; 20] {
|
|
||||||
[
|
|
||||||
OutputType::Vga,
|
|
||||||
OutputType::MiniDvi,
|
|
||||||
OutputType::Hdmi,
|
|
||||||
OutputType::Audio,
|
|
||||||
OutputType::OpticalAudio,
|
|
||||||
OutputType::Dvi,
|
|
||||||
OutputType::Thunderbolt,
|
|
||||||
OutputType::DisplayPort,
|
|
||||||
OutputType::MiniDisplayPort,
|
|
||||||
OutputType::FireWire400,
|
|
||||||
OutputType::Ps2,
|
|
||||||
OutputType::Sata,
|
|
||||||
OutputType::ESata,
|
|
||||||
OutputType::Ethernet,
|
|
||||||
OutputType::FireWire800,
|
|
||||||
OutputType::UsbTypeA,
|
|
||||||
OutputType::UsbTypeB,
|
|
||||||
OutputType::UsbTypeC,
|
|
||||||
OutputType::MicroUsb,
|
|
||||||
OutputType::MimiUsb,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Output {
|
|
||||||
#[serde(rename = "c")]
|
|
||||||
pub card: String,
|
|
||||||
#[serde(rename = "t")]
|
|
||||||
pub port_type: String,
|
|
||||||
#[serde(rename = "T")]
|
|
||||||
pub ty: Option<OutputType>,
|
|
||||||
#[serde(rename = "m")]
|
|
||||||
pub port_name: Option<String>,
|
|
||||||
#[serde(rename = "n")]
|
|
||||||
pub port_number: u8,
|
|
||||||
#[serde(rename = "s")]
|
|
||||||
pub status: Status,
|
|
||||||
#[serde(rename = "M")]
|
|
||||||
pub modes: Vec<OutputMode>,
|
|
||||||
pub display_power_managment: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct OutputMode {
|
|
||||||
#[serde(rename = "w")]
|
|
||||||
pub width: u16,
|
|
||||||
#[serde(rename = "h")]
|
|
||||||
pub height: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub enum Status {
|
|
||||||
#[serde(rename = "c")]
|
|
||||||
Connected,
|
|
||||||
#[serde(rename = "d")]
|
|
||||||
Disconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Status {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Output {
|
|
||||||
fn to_path(&self) -> PathBuf {
|
|
||||||
PathBuf::new().join("/sys/class/drm").join(format!(
|
|
||||||
"card{}-{}{}-{}",
|
|
||||||
self.card,
|
|
||||||
self.port_type,
|
|
||||||
self.port_name
|
|
||||||
.as_deref()
|
|
||||||
.map(|s| format!("-{s}"))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
self.port_number
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn status_path(&self) -> PathBuf {
|
|
||||||
self.to_path().join("status")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_status(&self) -> Option<Status> {
|
|
||||||
Some(
|
|
||||||
match std::fs::read_to_string(self.status_path()).ok()?.trim() {
|
|
||||||
"connected" => Status::Connected,
|
|
||||||
"disconnected" => Status::Disconnected,
|
|
||||||
_ => return None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Command {
|
|
||||||
Ports,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum Response {
|
|
||||||
Ports(Vec<Output>),
|
|
||||||
NoOp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PidResponse for Response {
|
|
||||||
fn kill_response() -> Self {
|
|
||||||
Self::NoOp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sock_file() -> PathBuf {
|
|
||||||
std::path::Path::new("/tmp").join("amdgpu-ports.sock")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_command(cmd: Command) -> crate::Result<Response> {
|
|
||||||
let sock_path = sock_file();
|
|
||||||
|
|
||||||
if !sock_path.exists() {
|
|
||||||
return Err(PortsError::NoSockFile.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(&sock_path).map_err(PortsError::UnableToConnect)?;
|
|
||||||
let s = ron::to_string(&cmd).map_err(PortsError::Serialize)?;
|
|
||||||
if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() {
|
|
||||||
log::info!("Command send");
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: Response = {
|
|
||||||
let mut s = String::with_capacity(100);
|
|
||||||
let _ = stream.read_to_string(&mut s);
|
|
||||||
ron::from_str(&s).map_err(PortsError::Serialize)?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
use serde::Serializer;
|
|
||||||
|
|
||||||
use crate::AmdGpuError;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
|
||||||
pub struct TempInput(pub u16);
|
|
||||||
|
|
||||||
impl TempInput {
|
|
||||||
pub fn as_string(&self) -> String {
|
|
||||||
format!("temp{}_input", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for TempInput {
|
|
||||||
type Err = AmdGpuError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if s.starts_with("temp") && s.ends_with("_input") {
|
|
||||||
let mut buffer = String::with_capacity(4);
|
|
||||||
for c in s[4..].chars() {
|
|
||||||
if c.is_numeric() {
|
|
||||||
buffer.push(c);
|
|
||||||
} else if buffer.is_empty() {
|
|
||||||
return Err(AmdGpuError::InvalidTempInput(s.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("Temp input error {:?}", e);
|
|
||||||
AmdGpuError::InvalidTempInput(s.to_string())
|
|
||||||
})
|
|
||||||
.map(Self)
|
|
||||||
} else {
|
|
||||||
Err(AmdGpuError::InvalidTempInput(s.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl serde::Serialize for TempInput {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
serializer.serialize_str(&self.as_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> serde::Deserialize<'de> for TempInput {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
use serde::de::{self, Visitor};
|
|
||||||
|
|
||||||
struct TempInputVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for TempInputVisitor {
|
|
||||||
type Value = u16;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("must have format cardX")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
match value.parse::<TempInput>() {
|
|
||||||
Ok(temp) => Ok(temp.0),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deserializer
|
|
||||||
.deserialize_str(TempInputVisitor)
|
|
||||||
.map(|v| TempInput(v as u16))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
use std::io::ErrorKind;
|
|
||||||
|
|
||||||
use crate::hw_mon::HwMon;
|
|
||||||
use crate::{hw_mon, Card, CONFIG_DIR, ROOT_DIR};
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AmdGpuError>;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AmdGpuError {
|
|
||||||
#[error("Write to {path:?} failed. {io}")]
|
|
||||||
Write { io: std::io::Error, path: String },
|
|
||||||
#[error("Read from {path:?} failed. {io}")]
|
|
||||||
Read { io: std::io::Error, path: String },
|
|
||||||
#[error("File {0:?} does not exists")]
|
|
||||||
FileNotFound(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_to_string<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
|
|
||||||
std::fs::read_to_string(&path).map_err(|io| {
|
|
||||||
if io.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default())
|
|
||||||
} else {
|
|
||||||
AmdGpuError::Read {
|
|
||||||
io,
|
|
||||||
path: path.as_ref().to_str().map(String::from).unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<P: AsRef<std::path::Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
|
|
||||||
std::fs::write(&path, contents).map_err(|io| {
|
|
||||||
if io.kind() == std::io::ErrorKind::NotFound {
|
|
||||||
AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default())
|
|
||||||
} else {
|
|
||||||
AmdGpuError::Write {
|
|
||||||
io,
|
|
||||||
path: path.as_ref().to_str().map(String::from).unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// linear mapping from the xrange to the yrange
|
|
||||||
pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 {
|
|
||||||
let m = (y2 - y1) / (x2 - x1);
|
|
||||||
m * (x - x1) + y1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read all available graphic cards from direct rendering manager
|
|
||||||
pub fn read_cards() -> std::io::Result<Vec<Card>> {
|
|
||||||
Ok(std::fs::read_dir(ROOT_DIR)?
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|entry| entry.ok())
|
|
||||||
.filter_map(|entry| entry.file_name().as_os_str().to_str().map(String::from))
|
|
||||||
.filter_map(|file_name| file_name.parse::<Card>().ok())
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap cards in HW Mon manipulator and
|
|
||||||
/// filter cards so only amd and listed in config cards are accessible
|
|
||||||
pub fn hw_mons(filter: bool) -> std::io::Result<Vec<HwMon>> {
|
|
||||||
Ok(read_cards()?
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|card| {
|
|
||||||
log::info!("opening hw mon for {:?}", card);
|
|
||||||
hw_mon::open_hw_mon(card)
|
|
||||||
})
|
|
||||||
.filter(|hw_mon| {
|
|
||||||
!filter || {
|
|
||||||
log::info!("is vendor ok? {}", hw_mon.is_amd());
|
|
||||||
hw_mon.is_amd()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(|hw_mon| {
|
|
||||||
!filter || {
|
|
||||||
log::info!("is hwmon name ok? {}", hw_mon.name_is_amd());
|
|
||||||
hw_mon.name_is_amd()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to read from config file or create new config file.
|
|
||||||
/// Create only if it does not exists, malformed file will raise error
|
|
||||||
pub fn ensure_config<Config, Error, P>(config_path: P) -> std::result::Result<Config, Error>
|
|
||||||
where
|
|
||||||
Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized,
|
|
||||||
P: AsRef<std::path::Path>,
|
|
||||||
Error: From<std::io::Error>,
|
|
||||||
{
|
|
||||||
match std::fs::read_to_string(&config_path) {
|
|
||||||
Ok(s) => Ok(toml::from_str::<Config>(s.as_str()).unwrap()),
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
let config = Config::default();
|
|
||||||
std::fs::write(config_path, toml::to_string(&config).unwrap())?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{:?}", e);
|
|
||||||
panic!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan sysfs for sensor files
|
|
||||||
pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec<String> {
|
|
||||||
let dir = match std::fs::read_dir(hw_mon.mon_dir()) {
|
|
||||||
Ok(d) => d,
|
|
||||||
_ => return vec![],
|
|
||||||
};
|
|
||||||
dir.filter_map(|f| f.ok())
|
|
||||||
.filter_map(|f| {
|
|
||||||
f.file_name()
|
|
||||||
.to_str()
|
|
||||||
.filter(|s| s.starts_with("temp") && s.ends_with("_input"))
|
|
||||||
.map(String::from)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create config directory if does not exists
|
|
||||||
pub fn ensure_config_dir() -> std::io::Result<()> {
|
|
||||||
if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) {
|
|
||||||
std::fs::create_dir_all(CONFIG_DIR)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-musl"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,33 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdgui-helper"
|
|
||||||
version = "1.0.10"
|
|
||||||
edition = "2018"
|
|
||||||
description = "AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
ron = { version = "0.7" }
|
|
||||||
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
nix = { version = "0.23" }
|
|
||||||
|
|
||||||
sudo = { version = "0.6" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
|
@ -1,7 +0,0 @@
|
|||||||
# amdgui-helper
|
|
||||||
|
|
||||||
Daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files
|
|
||||||
|
|
||||||
You can communicate with it using sock file `/tmp/amdgui-helper.sock` using helper `Command` from `amdgpu`.
|
|
||||||
|
|
||||||
Each connection is single use and will be terminated after sending `Response`.
|
|
@ -1,127 +0,0 @@
|
|||||||
//! Special daemon with root privileges. Since GUI should not have (and
|
|
||||||
//! sometimes can't have) root privileges and service processes are designed to
|
|
||||||
//! be as small as possible this is proxy.
|
|
||||||
//!
|
|
||||||
//! It is responsible for:
|
|
||||||
//! * Loading all amdfand processes. In order to do this process needs to be
|
|
||||||
//! killed with signal 0 to check if it still is alive
|
|
||||||
//! * Reload amdfand process with signal SIGHUP
|
|
||||||
//! * Save changed config file
|
|
||||||
//!
|
|
||||||
//! It is using `/tmp/amdgui-helper.sock` file and `ron` serialization for
|
|
||||||
//! communication. After each operation connection is terminated so each command
|
|
||||||
//! needs new connection.
|
|
||||||
#![allow(clippy::non_octal_unix_permissions)]
|
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::fs::Permissions;
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::os::unix::net::UnixListener;
|
|
||||||
|
|
||||||
use amdgpu::pidfile::helper_cmd::{Command, Response};
|
|
||||||
use amdgpu::pidfile::{handle_connection, Pid};
|
|
||||||
use amdgpu::IoFailure;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] amdgpu::IoFailure),
|
|
||||||
#[error("{0}")]
|
|
||||||
Lock(#[from] amdgpu::AmdGpuError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "DEBUG");
|
|
||||||
}
|
|
||||||
pretty_env_logger::init();
|
|
||||||
|
|
||||||
let mut lock = amdgpu::lock_file::PidLock::new("amdgui", String::from("helper"))?;
|
|
||||||
lock.acquire()?;
|
|
||||||
|
|
||||||
let sock_path = amdgpu::pidfile::helper_cmd::sock_file();
|
|
||||||
let listener = {
|
|
||||||
let _ = std::fs::remove_file(&sock_path);
|
|
||||||
|
|
||||||
UnixListener::bind(&sock_path).map_err(|io| IoFailure {
|
|
||||||
io,
|
|
||||||
path: sock_path.clone(),
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) {
|
|
||||||
log::error!("Failed to change gui helper socket file mode. {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Ok((stream, _addr)) = listener.accept() {
|
|
||||||
handle_connection::<_, Command, Response>(stream, handle_command);
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.release()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Service = amdgpu::pidfile::Service<Response>;
|
|
||||||
|
|
||||||
fn handle_command(service: Service, cmd: Command) {
|
|
||||||
match cmd {
|
|
||||||
Command::ReloadConfig { pid } => {
|
|
||||||
log::info!("Reloading config file for pid {:?}", pid);
|
|
||||||
handle_reload_config(service, pid);
|
|
||||||
}
|
|
||||||
Command::FanServices => handle_fan_services(service),
|
|
||||||
Command::SaveFanConfig { path, content } => handle_save_fan_config(service, path, content),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_save_fan_config(mut service: Service, path: String, content: String) {
|
|
||||||
match std::fs::write(path, content) {
|
|
||||||
Err(e) => service.write_response(Response::ConfigFileSaveFailed(format!("{:?}", e))),
|
|
||||||
Ok(..) => service.write_response(Response::ConfigFileSaved),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_fan_services(mut service: Service) {
|
|
||||||
log::info!("Loading fan services");
|
|
||||||
let services = read_fan_services();
|
|
||||||
log::info!("Loaded fan services pid {:?}", services);
|
|
||||||
service.write_response(Response::Services(services));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_reload_config(service: Service, pid: Pid) {
|
|
||||||
unsafe {
|
|
||||||
nix::libc::kill(pid.0, nix::sys::signal::Signal::SIGHUP as i32);
|
|
||||||
}
|
|
||||||
service.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_fan_services() -> Vec<Pid> {
|
|
||||||
if let Ok(entry) = std::fs::read_dir("/var/lib/amdfand") {
|
|
||||||
entry
|
|
||||||
.filter(|e| {
|
|
||||||
e.as_ref()
|
|
||||||
.map(|e| {
|
|
||||||
log::info!("Extension is {:?}", e.path().extension());
|
|
||||||
e.path().extension().and_then(OsStr::to_str) == Some("pid")
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.filter_map(|e| {
|
|
||||||
log::info!("Found entry {:?}", e);
|
|
||||||
match e {
|
|
||||||
Ok(entry) => std::fs::read_to_string(entry.path())
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.parse::<i32>().ok())
|
|
||||||
.filter(|pid| unsafe { nix::libc::kill(*pid, 0) } == 0),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(Pid)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
log::warn!("Directory /var/lib/amdfand not found");
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-gnu"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-gnu"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,73 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdguid"
|
|
||||||
version = "1.0.12"
|
|
||||||
edition = "2018"
|
|
||||||
description = "AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
wayland = [
|
|
||||||
"egui_vulkano",
|
|
||||||
"vulkano-win",
|
|
||||||
"vulkano",
|
|
||||||
"vulkano-shaders",
|
|
||||||
"_gui"
|
|
||||||
]
|
|
||||||
xorg-glium = ["glium", "egui_glium", "_gui"]
|
|
||||||
xorg-glow = ["glow", "egui_glow", "glutin", "_gui"]
|
|
||||||
default = ["wayland"]
|
|
||||||
_gui = [
|
|
||||||
"egui",
|
|
||||||
"epaint",
|
|
||||||
"epi",
|
|
||||||
"winit",
|
|
||||||
"egui-winit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan", "gui"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.10" }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
tracing = { version = "0.1.36" }
|
|
||||||
tracing-subscriber = { version = "0.3.15" }
|
|
||||||
|
|
||||||
egui = { version = "0.18", optional = true, features = [] }
|
|
||||||
epaint = { version = "0.18", features = [], optional = true }
|
|
||||||
epi = { version = "0.17.0", optional = true }
|
|
||||||
winit = { version = "0.26", optional = true }
|
|
||||||
egui-winit = { version = "0.18", optional = true }
|
|
||||||
# vulkan
|
|
||||||
egui_vulkano = { version = "0.8.0", optional = true }
|
|
||||||
vulkano-win = { version = "0.29.0", optional = true }
|
|
||||||
vulkano = { version = "0.29.0", optional = true }
|
|
||||||
vulkano-shaders = { version = "0.29.0", optional = true }
|
|
||||||
bytemuck = { version = "*" }
|
|
||||||
# xorg glium
|
|
||||||
glium = { version = "0.32.1", optional = true }
|
|
||||||
egui_glium = { version = "0.18.0", optional = true }
|
|
||||||
# xorg glow
|
|
||||||
glutin = { version = "0.29", optional = true }
|
|
||||||
glow = { version = "0.11", optional = true }
|
|
||||||
egui_glow = { version = "0.18", optional = true }
|
|
||||||
|
|
||||||
tokio = { version = "1.15", features = ["full"] }
|
|
||||||
parking_lot = { version = "0.12" }
|
|
||||||
|
|
||||||
nix = { version = "0.25" }
|
|
||||||
|
|
||||||
image = { version = "0.24.2" }
|
|
||||||
emath = { version = "0.18" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
|
@ -1,15 +0,0 @@
|
|||||||
# AMD GPU gui tool
|
|
||||||
|
|
||||||
Provides basic FAN configuration.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
* amdvold config manipulation
|
|
||||||
* Fix Drag & drop functionality - mouse is not followed properly
|
|
||||||
* Program profiles
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png)
|
|
||||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png)
|
|
||||||
![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png)
|
|
Binary file not shown.
Before Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 108 KiB |
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.2 KiB |
@ -1,317 +0,0 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use amdgpu::pidfile::ports::{Output, OutputType};
|
|
||||||
use amdgpu::pidfile::Pid;
|
|
||||||
use egui::Ui;
|
|
||||||
use epaint::ColorImage;
|
|
||||||
use epi::Frame;
|
|
||||||
use image::{GenericImageView, ImageBuffer, ImageFormat};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
|
|
||||||
use crate::widgets::outputs_settings::OutputsSettings;
|
|
||||||
use crate::widgets::{ChangeFanSettings, CoolingPerformance};
|
|
||||||
|
|
||||||
pub enum ChangeState {
|
|
||||||
New,
|
|
||||||
Reloading,
|
|
||||||
Success,
|
|
||||||
Failure(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ChangeState {
|
|
||||||
fn default() -> Self {
|
|
||||||
ChangeState::New
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FanService {
|
|
||||||
pub pid: Pid,
|
|
||||||
pub reload: ChangeState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FanService {
|
|
||||||
pub fn new(pid: Pid) -> FanService {
|
|
||||||
Self {
|
|
||||||
pid,
|
|
||||||
reload: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FanServices(pub Vec<FanService>);
|
|
||||||
|
|
||||||
impl std::ops::Deref for FanServices {
|
|
||||||
type Target = Vec<FanService>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FanServices {
|
|
||||||
pub fn list_changed(&self, other: &[Pid]) -> bool {
|
|
||||||
if self.0.len() != other.len() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let c = self
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.fold(HashMap::with_capacity(other.len()), |mut h, service| {
|
|
||||||
h.insert(service.pid.0, true);
|
|
||||||
h
|
|
||||||
});
|
|
||||||
!other.iter().all(|s| c.contains_key(&s.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<Pid>> for FanServices {
|
|
||||||
fn from(v: Vec<Pid>) -> Self {
|
|
||||||
Self(v.into_iter().map(FanService::new).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum Page {
|
|
||||||
Config,
|
|
||||||
Monitoring,
|
|
||||||
Outputs,
|
|
||||||
Settings,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Page {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type FanConfig = Arc<Mutex<amdgpu_config::fan::Config>>;
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
static RELOAD_PID_LIST_DELAY: u8 = 18;
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
static RELOAD_PID_LIST_DELAY: u8 = 80;
|
|
||||||
|
|
||||||
pub struct StatefulConfig {
|
|
||||||
pub config: FanConfig,
|
|
||||||
pub state: ChangeState,
|
|
||||||
pub textures: HashMap<OutputType, epaint::TextureHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StatefulConfig {
|
|
||||||
pub fn new(config: FanConfig) -> Self {
|
|
||||||
let textures = HashMap::with_capacity(40);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
state: ChangeState::New,
|
|
||||||
textures,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_textures(&mut self, ui: &mut Ui) {
|
|
||||||
if !self.textures.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 80x80
|
|
||||||
let image = {
|
|
||||||
let bytes = include_bytes!("../assets/icons/ports2.png");
|
|
||||||
image::load_from_memory_with_format(bytes, ImageFormat::Png).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let ctx = ui.ctx();
|
|
||||||
|
|
||||||
for ty in OutputType::all() {
|
|
||||||
let (offset_x, offset_y) = ty.to_coords();
|
|
||||||
let mut img = ImageBuffer::new(80, 80);
|
|
||||||
for x in 0..80 {
|
|
||||||
for y in 0..80 {
|
|
||||||
img.put_pixel(x, y, image.get_pixel(x + offset_x, y + offset_y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = [img.width() as _, img.height() as _];
|
|
||||||
let pixels = img.as_flat_samples();
|
|
||||||
let id = ctx.load_texture(
|
|
||||||
String::from(ty.name()),
|
|
||||||
epaint::ImageData::Color(ColorImage::from_rgba_unmultiplied(
|
|
||||||
size,
|
|
||||||
pixels.as_slice(),
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
self.textures.insert(ty, id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AmdGui {
|
|
||||||
pub page: Page,
|
|
||||||
pid_files: SocketState<FanServices>,
|
|
||||||
outputs: SocketState<BTreeMap<String, Vec<Output>>>,
|
|
||||||
cooling_performance: CoolingPerformance,
|
|
||||||
change_fan_settings: ChangeFanSettings,
|
|
||||||
outputs_settings: OutputsSettings,
|
|
||||||
config: StatefulConfig,
|
|
||||||
reload_pid_list_delay: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl epi::App for AmdGui {
|
|
||||||
fn update(&mut self, _ctx: &epi::egui::Context, _frame: &Frame) {}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
"AMD GUI"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AmdGui {
|
|
||||||
pub fn new_with_config(config: FanConfig) -> Self {
|
|
||||||
Self {
|
|
||||||
page: Default::default(),
|
|
||||||
pid_files: SocketState::NotAvailable,
|
|
||||||
outputs: SocketState::NotAvailable,
|
|
||||||
cooling_performance: CoolingPerformance::new(100, config.clone()),
|
|
||||||
change_fan_settings: ChangeFanSettings::new(config.clone()),
|
|
||||||
outputs_settings: OutputsSettings::default(),
|
|
||||||
config: StatefulConfig::new(config),
|
|
||||||
reload_pid_list_delay: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut Ui) {
|
|
||||||
self.config.load_textures(ui);
|
|
||||||
|
|
||||||
match self.page {
|
|
||||||
Page::Config => {
|
|
||||||
if let SocketState::Connected(pid_files) = &mut self.pid_files {
|
|
||||||
self.change_fan_settings
|
|
||||||
.draw(ui, pid_files, &mut self.config);
|
|
||||||
} else {
|
|
||||||
ui.label("Not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Page::Monitoring => {
|
|
||||||
if let SocketState::Connected(pid_files) = &mut self.pid_files {
|
|
||||||
self.cooling_performance.draw(ui, pid_files);
|
|
||||||
} else {
|
|
||||||
ui.label("Not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Page::Settings => {}
|
|
||||||
Page::Outputs => {
|
|
||||||
if let SocketState::Connected(outputs) = &self.outputs {
|
|
||||||
self.outputs_settings.draw(ui, &mut self.config, outputs);
|
|
||||||
} else {
|
|
||||||
ui.label("Not available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
|
||||||
self.cooling_performance.tick();
|
|
||||||
let can_decrease = self.reload_pid_list_delay > 0;
|
|
||||||
|
|
||||||
if can_decrease {
|
|
||||||
self.reload_pid_list_delay -= 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reload_pid_list_delay = RELOAD_PID_LIST_DELAY;
|
|
||||||
|
|
||||||
{
|
|
||||||
use amdgpu::pidfile::helper_cmd::{send_command, Command, Response};
|
|
||||||
|
|
||||||
match send_command(Command::FanServices) {
|
|
||||||
Ok(Response::Services(services))
|
|
||||||
if self
|
|
||||||
.pid_files
|
|
||||||
.connected()
|
|
||||||
.map(|c| c.list_changed(&services))
|
|
||||||
.unwrap_or(true) =>
|
|
||||||
{
|
|
||||||
self.pid_files = SocketState::Connected(FanServices::from(services));
|
|
||||||
}
|
|
||||||
Ok(Response::Services(_services)) => {
|
|
||||||
// SKIP
|
|
||||||
}
|
|
||||||
Ok(res) => {
|
|
||||||
tracing::warn!("Unexpected response {:?} while loading fan services", res);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.pid_files = SocketState::NotAvailable;
|
|
||||||
tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
use amdgpu::pidfile::ports::{send_command, Command, Response};
|
|
||||||
|
|
||||||
match send_command(Command::Ports) {
|
|
||||||
Ok(Response::NoOp) => {}
|
|
||||||
Ok(Response::Ports(outputs)) => {
|
|
||||||
let mut names = outputs.iter().fold(
|
|
||||||
Vec::with_capacity(outputs.len()),
|
|
||||||
|mut set, output| {
|
|
||||||
set.push(output.card.clone());
|
|
||||||
set
|
|
||||||
},
|
|
||||||
);
|
|
||||||
names.sort();
|
|
||||||
|
|
||||||
let mut tree = BTreeMap::new();
|
|
||||||
names.into_iter().for_each(|name| {
|
|
||||||
tree.insert(name, Vec::with_capacity(6));
|
|
||||||
});
|
|
||||||
|
|
||||||
self.outputs = SocketState::Connected(outputs.into_iter().fold(
|
|
||||||
tree,
|
|
||||||
|mut agg, output| {
|
|
||||||
let v = agg
|
|
||||||
.entry(output.card.clone())
|
|
||||||
.or_insert_with(|| Vec::with_capacity(6));
|
|
||||||
v.push(output);
|
|
||||||
v.sort_by(|a, b| {
|
|
||||||
format!(
|
|
||||||
"{}{}{}",
|
|
||||||
a.port_type,
|
|
||||||
a.port_name.as_deref().unwrap_or_default(),
|
|
||||||
a.port_number,
|
|
||||||
)
|
|
||||||
.cmp(&format!(
|
|
||||||
"{}{}{}",
|
|
||||||
b.port_type,
|
|
||||||
b.port_name.as_deref().unwrap_or_default(),
|
|
||||||
b.port_number,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
agg
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if matches!(self.page, Page::Outputs) {
|
|
||||||
self.page = Page::Config;
|
|
||||||
}
|
|
||||||
self.outputs = SocketState::NotAvailable;
|
|
||||||
tracing::warn!("Failed to load amd fan services pid list. {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SocketState<Content> {
|
|
||||||
NotAvailable,
|
|
||||||
Connected(Content),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> SocketState<C> {
|
|
||||||
pub fn connected(&self) -> Option<&C> {
|
|
||||||
match self {
|
|
||||||
Self::Connected(c) => Some(c),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use glium::glutin;
|
|
||||||
use image::RgbaImage;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
|
||||||
|
|
||||||
use crate::app::{AmdGui, ImageStorage, ImageType};
|
|
||||||
use crate::backend::create_ui;
|
|
||||||
|
|
||||||
fn create_display(event_loop: &glutin::event_loop::EventLoop<()>) -> glium::Display {
|
|
||||||
let window_builder = glutin::window::WindowBuilder::new()
|
|
||||||
.with_resizable(true)
|
|
||||||
.with_inner_size(glutin::dpi::LogicalSize {
|
|
||||||
width: 800.0,
|
|
||||||
height: 600.0,
|
|
||||||
})
|
|
||||||
.with_title("AMD GUI");
|
|
||||||
|
|
||||||
let context_builder = glutin::ContextBuilder::new()
|
|
||||||
.with_depth_buffer(0)
|
|
||||||
.with_srgb(true)
|
|
||||||
.with_stencil_buffer(0)
|
|
||||||
.with_vsync(true);
|
|
||||||
|
|
||||||
glium::Display::new(window_builder, context_builder, event_loop).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>, mut receiver: UnboundedReceiver<bool>) {
|
|
||||||
let event_loop = glutin::event_loop::EventLoop::with_user_event();
|
|
||||||
let display = create_display(&event_loop);
|
|
||||||
|
|
||||||
let mut egui = egui_glium::EguiGlium::new(&display);
|
|
||||||
|
|
||||||
let proxy = event_loop.create_proxy();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
if receiver.recv().await.is_some() {
|
|
||||||
if let Err(e) = proxy.send_event(()) {
|
|
||||||
tracing::error!("{:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
|
||||||
let mut redraw = || {
|
|
||||||
egui.begin_frame(&display);
|
|
||||||
|
|
||||||
create_ui(amd_gui.clone(), egui.ctx());
|
|
||||||
|
|
||||||
let (needs_repaint, shapes) = egui.end_frame(&display);
|
|
||||||
|
|
||||||
*control_flow = if needs_repaint {
|
|
||||||
display.gl_window().window().request_redraw();
|
|
||||||
glutin::event_loop::ControlFlow::Poll
|
|
||||||
} else {
|
|
||||||
glutin::event_loop::ControlFlow::Wait
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
use glium::Surface as _;
|
|
||||||
let mut target = display.draw();
|
|
||||||
|
|
||||||
let color = egui::Rgba::from_rgb(0.1, 0.3, 0.2);
|
|
||||||
target.clear_color(color[0], color[1], color[2], color[3]);
|
|
||||||
egui.paint(&display, &mut target, shapes);
|
|
||||||
target.finish().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
glutin::event::Event::UserEvent(_) | glutin::event::Event::RedrawRequested(_) => {
|
|
||||||
redraw()
|
|
||||||
}
|
|
||||||
glutin::event::Event::WindowEvent { event, .. } => {
|
|
||||||
if egui.is_quit_event(&event) {
|
|
||||||
*control_flow = glium::glutin::event_loop::ControlFlow::Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
egui.on_event(&event);
|
|
||||||
|
|
||||||
display.gl_window().window().request_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
|
||||||
|
|
||||||
use crate::backend::create_ui;
|
|
||||||
use crate::AmdGui;
|
|
||||||
|
|
||||||
fn create_display(
|
|
||||||
event_loop: &glutin::event_loop::EventLoop<()>,
|
|
||||||
) -> (
|
|
||||||
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
|
||||||
::glow::Context,
|
|
||||||
) {
|
|
||||||
let window_builder = glutin::window::WindowBuilder::new()
|
|
||||||
.with_resizable(true)
|
|
||||||
.with_inner_size(glutin::dpi::LogicalSize {
|
|
||||||
width: 800.0,
|
|
||||||
height: 600.0,
|
|
||||||
})
|
|
||||||
.with_title("AMD GUI");
|
|
||||||
|
|
||||||
let gl_window = unsafe {
|
|
||||||
glutin::ContextBuilder::new()
|
|
||||||
.with_depth_buffer(0)
|
|
||||||
.with_srgb(true)
|
|
||||||
.with_stencil_buffer(0)
|
|
||||||
.with_vsync(true)
|
|
||||||
.build_windowed(window_builder, event_loop)
|
|
||||||
.unwrap()
|
|
||||||
.make_current()
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let gl = unsafe { ::glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) };
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
use glow::HasContext as _;
|
|
||||||
gl.enable(glow::FRAMEBUFFER_SRGB);
|
|
||||||
}
|
|
||||||
|
|
||||||
(gl_window, gl)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>, mut receiver: UnboundedReceiver<bool>) {
|
|
||||||
let event_loop = glutin::event_loop::EventLoop::with_user_event();
|
|
||||||
let (gl_window, gl) = create_display(&event_loop);
|
|
||||||
|
|
||||||
let mut egui = egui_glow::EguiGlow::new(&gl_window, &gl);
|
|
||||||
let proxy = event_loop.create_proxy();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
if receiver.recv().await.is_some() {
|
|
||||||
if let Err(e) = proxy.send_event(()) {
|
|
||||||
tracing::error!("{:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
|
||||||
let mut redraw = || {
|
|
||||||
egui.begin_frame(gl_window.window());
|
|
||||||
|
|
||||||
create_ui(amd_gui.clone(), egui.ctx());
|
|
||||||
|
|
||||||
let (needs_repaint, shapes) = egui.end_frame(gl_window.window());
|
|
||||||
|
|
||||||
*control_flow = if needs_repaint {
|
|
||||||
gl_window.window().request_redraw();
|
|
||||||
glutin::event_loop::ControlFlow::Poll
|
|
||||||
} else {
|
|
||||||
glutin::event_loop::ControlFlow::Wait
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let color = egui::Rgba::from_rgb(0.1, 0.3, 0.2);
|
|
||||||
unsafe {
|
|
||||||
use glow::HasContext as _;
|
|
||||||
gl.clear_color(color[0], color[1], color[2], color[3]);
|
|
||||||
gl.clear(glow::COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw things behind egui here
|
|
||||||
|
|
||||||
egui.paint(&gl_window, &gl, shapes);
|
|
||||||
|
|
||||||
// draw things on top of egui here
|
|
||||||
|
|
||||||
gl_window.swap_buffers().unwrap();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
glutin::event::Event::UserEvent(_) | glutin::event::Event::RedrawRequested(_) => {
|
|
||||||
redraw()
|
|
||||||
}
|
|
||||||
|
|
||||||
glutin::event::Event::WindowEvent { event, .. } => {
|
|
||||||
if egui.is_quit_event(&event) {
|
|
||||||
*control_flow = glutin::event_loop::ControlFlow::Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let glutin::event::WindowEvent::Resized(physical_size) = event {
|
|
||||||
gl_window.resize(physical_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
egui.on_event(&event);
|
|
||||||
|
|
||||||
gl_window.window().request_redraw(); // TODO: ask egui if the
|
|
||||||
// events warrants a
|
|
||||||
// repaint instead
|
|
||||||
}
|
|
||||||
glutin::event::Event::LoopDestroyed => {
|
|
||||||
egui.destroy(&gl);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
#[cfg(feature = "xorg-glium")]
|
|
||||||
pub mod glium_backend;
|
|
||||||
#[cfg(feature = "xorg-glow")]
|
|
||||||
pub mod glow_backend;
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
pub mod wayland_backend;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use egui::panel::TopBottomSide;
|
|
||||||
use egui::{Layout, PointerButton};
|
|
||||||
#[cfg(feature = "xorg-glium")]
|
|
||||||
pub use glium_backend::*;
|
|
||||||
#[cfg(feature = "xorg-glow")]
|
|
||||||
pub use glow_backend::*;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
#[cfg(feature = "wayland")]
|
|
||||||
pub use wayland_backend::*;
|
|
||||||
|
|
||||||
use crate::app::Page;
|
|
||||||
use crate::AmdGui;
|
|
||||||
|
|
||||||
pub fn create_ui(amd_gui: Arc<Mutex<AmdGui>>, ctx: &egui::Context) {
|
|
||||||
egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show(ctx, |ui| {
|
|
||||||
let mut child = ui.child_ui(ui.available_rect_before_wrap(), Layout::left_to_right());
|
|
||||||
|
|
||||||
if child
|
|
||||||
.add(
|
|
||||||
egui::Button::new("Config"), /* .text_style(TextStyle::Heading) */
|
|
||||||
)
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
amd_gui.lock().page = Page::Config;
|
|
||||||
}
|
|
||||||
if child
|
|
||||||
.add(
|
|
||||||
egui::Button::new("Monitoring"), /* .text_style(TextStyle::Heading) */
|
|
||||||
)
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
amd_gui.lock().page = Page::Monitoring;
|
|
||||||
}
|
|
||||||
if child
|
|
||||||
.add(
|
|
||||||
egui::Button::new("Outputs"), /* .text_style(TextStyle::Heading) */
|
|
||||||
)
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
amd_gui.lock().page = Page::Outputs;
|
|
||||||
}
|
|
||||||
if child
|
|
||||||
.add(
|
|
||||||
egui::Button::new("Settings"), /* .text_style(TextStyle::Heading) */
|
|
||||||
)
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
amd_gui.lock().page = Page::Settings;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
egui::containers::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
let mut gui = amd_gui.lock();
|
|
||||||
let page = gui.page;
|
|
||||||
match page {
|
|
||||||
Page::Config => {
|
|
||||||
gui.ui(ui);
|
|
||||||
}
|
|
||||||
Page::Monitoring => {
|
|
||||||
gui.ui(ui);
|
|
||||||
}
|
|
||||||
Page::Outputs => {
|
|
||||||
gui.ui(ui);
|
|
||||||
}
|
|
||||||
Page::Settings => {
|
|
||||||
ctx.settings_ui(ui);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,447 +0,0 @@
|
|||||||
use std::convert::{TryFrom, TryInto};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
|
||||||
use egui_vulkano::UpdateTexturesResult;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
|
||||||
use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer, TypedBufferAccess};
|
|
||||||
use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, SubpassContents};
|
|
||||||
use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType};
|
|
||||||
use vulkano::device::{Device, DeviceCreateInfo, DeviceExtensions, QueueCreateInfo};
|
|
||||||
use vulkano::format::Format;
|
|
||||||
use vulkano::image::view::ImageView;
|
|
||||||
use vulkano::image::{ImageAccess, ImageUsage, SwapchainImage};
|
|
||||||
use vulkano::instance::{Instance, InstanceCreateInfo};
|
|
||||||
use vulkano::pipeline::graphics::input_assembly::InputAssemblyState;
|
|
||||||
use vulkano::pipeline::graphics::vertex_input::BuffersDefinition;
|
|
||||||
use vulkano::pipeline::graphics::viewport::{Viewport, ViewportState};
|
|
||||||
use vulkano::pipeline::GraphicsPipeline;
|
|
||||||
use vulkano::render_pass::{Framebuffer, FramebufferCreateInfo, RenderPass, Subpass};
|
|
||||||
use vulkano::swapchain::{AcquireError, Swapchain, SwapchainCreateInfo, SwapchainCreationError};
|
|
||||||
use vulkano::sync::{FenceSignalFuture, FlushError, GpuFuture};
|
|
||||||
use vulkano::{swapchain, sync};
|
|
||||||
use vulkano_win::VkSurfaceBuild;
|
|
||||||
use winit::event::{Event, WindowEvent};
|
|
||||||
use winit::event_loop::{ControlFlow, EventLoop};
|
|
||||||
use winit::window::{Window, WindowBuilder};
|
|
||||||
|
|
||||||
use crate::app::AmdGui;
|
|
||||||
use crate::backend::create_ui;
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
|
|
||||||
struct Vertex {
|
|
||||||
position: [f32; 2],
|
|
||||||
color: [f32; 4],
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum FrameEndFuture<F: GpuFuture + 'static> {
|
|
||||||
FenceSignalFuture(FenceSignalFuture<F>),
|
|
||||||
BoxedFuture(Box<dyn GpuFuture>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: GpuFuture> FrameEndFuture<F> {
|
|
||||||
pub fn now(device: Arc<Device>) -> Self {
|
|
||||||
Self::BoxedFuture(sync::now(device).boxed())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(self) -> Box<dyn GpuFuture> {
|
|
||||||
match self {
|
|
||||||
FrameEndFuture::FenceSignalFuture(f) => f.boxed(),
|
|
||||||
FrameEndFuture::BoxedFuture(f) => f,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<F: GpuFuture> AsMut<dyn GpuFuture> for FrameEndFuture<F> {
|
|
||||||
fn as_mut(&mut self) -> &mut (dyn GpuFuture + 'static) {
|
|
||||||
match self {
|
|
||||||
FrameEndFuture::FenceSignalFuture(f) => f,
|
|
||||||
FrameEndFuture::BoxedFuture(f) => f,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_app(amd_gui: Arc<Mutex<AmdGui>>, _receiver: UnboundedReceiver<bool>) {
|
|
||||||
let required_extensions = vulkano_win::required_extensions();
|
|
||||||
let instance = Instance::new(InstanceCreateInfo {
|
|
||||||
application_name: Some("amdguid".into()),
|
|
||||||
enabled_extensions: required_extensions,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let physical = {
|
|
||||||
let mut v = PhysicalDevice::enumerate(&instance).collect::<Vec<_>>();
|
|
||||||
v.sort_by(|a, b| a.api_version().cmp(&b.api_version()));
|
|
||||||
v.remove(0)
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Using device: {} (type: {:?})",
|
|
||||||
physical.properties().device_name,
|
|
||||||
physical.properties().device_type,
|
|
||||||
);
|
|
||||||
|
|
||||||
let event_loop = EventLoop::new();
|
|
||||||
let surface = WindowBuilder::new()
|
|
||||||
.with_title("AMD GUID")
|
|
||||||
// .with_fullscreen(Some(Fullscreen::Borderless(None)))
|
|
||||||
.build_vk_surface(&event_loop, instance.clone())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let device_extensions = DeviceExtensions {
|
|
||||||
khr_swapchain: true,
|
|
||||||
..DeviceExtensions::none()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (physical_device, queue_family) = PhysicalDevice::enumerate(&instance)
|
|
||||||
.filter(|&p| p.supported_extensions().is_superset_of(&device_extensions))
|
|
||||||
.filter_map(|p| {
|
|
||||||
p.queue_families()
|
|
||||||
.find(|&q| q.supports_graphics() && q.supports_surface(&surface).unwrap_or(false))
|
|
||||||
.map(|q| (p, q))
|
|
||||||
})
|
|
||||||
.min_by_key(|(p, _)| match p.properties().device_type {
|
|
||||||
PhysicalDeviceType::DiscreteGpu => 0,
|
|
||||||
PhysicalDeviceType::IntegratedGpu => 1,
|
|
||||||
PhysicalDeviceType::VirtualGpu => 2,
|
|
||||||
PhysicalDeviceType::Cpu => 3,
|
|
||||||
PhysicalDeviceType::Other => 4,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (device, mut queues) = Device::new(
|
|
||||||
physical_device,
|
|
||||||
DeviceCreateInfo {
|
|
||||||
enabled_extensions: physical_device
|
|
||||||
.required_extensions()
|
|
||||||
.union(&device_extensions),
|
|
||||||
queue_create_infos: vec![QueueCreateInfo::family(queue_family)],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let queue = queues.next().unwrap();
|
|
||||||
|
|
||||||
let (mut swapchain, images) = {
|
|
||||||
let caps = physical_device
|
|
||||||
.surface_capabilities(&surface, Default::default())
|
|
||||||
.unwrap();
|
|
||||||
let composite_alpha = caps.supported_composite_alpha.iter().next().unwrap();
|
|
||||||
|
|
||||||
let image_format = Some(Format::B8G8R8A8_SRGB);
|
|
||||||
let image_extent: [u32; 2] = surface.window().inner_size().into();
|
|
||||||
|
|
||||||
Swapchain::new(
|
|
||||||
device.clone(),
|
|
||||||
surface.clone(),
|
|
||||||
SwapchainCreateInfo {
|
|
||||||
min_image_count: caps.min_image_count,
|
|
||||||
image_format,
|
|
||||||
image_extent,
|
|
||||||
image_usage: ImageUsage::color_attachment(),
|
|
||||||
composite_alpha,
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Pod, Zeroable)]
|
|
||||||
#[repr(C)]
|
|
||||||
struct Vertex {
|
|
||||||
position: [f32; 2],
|
|
||||||
}
|
|
||||||
vulkano::impl_vertex!(Vertex, position);
|
|
||||||
|
|
||||||
let vertex_buffer = {
|
|
||||||
CpuAccessibleBuffer::from_iter(
|
|
||||||
device.clone(),
|
|
||||||
BufferUsage::all(),
|
|
||||||
false,
|
|
||||||
[
|
|
||||||
Vertex {
|
|
||||||
position: [-0.5, -0.25],
|
|
||||||
},
|
|
||||||
Vertex {
|
|
||||||
position: [0.0, 0.5],
|
|
||||||
},
|
|
||||||
Vertex {
|
|
||||||
position: [0.25, -0.1],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.cloned(),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
mod vs {
|
|
||||||
#![allow(clippy::needless_question_mark)]
|
|
||||||
vulkano_shaders::shader! {
|
|
||||||
ty: "vertex",
|
|
||||||
src: "
|
|
||||||
#version 450
|
|
||||||
|
|
||||||
layout(location = 0) in vec2 position;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(position, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod fs {
|
|
||||||
#![allow(clippy::needless_question_mark)]
|
|
||||||
vulkano_shaders::shader! {
|
|
||||||
ty: "fragment",
|
|
||||||
src: "
|
|
||||||
#version 450
|
|
||||||
|
|
||||||
layout(location = 0) out vec4 f_color;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
f_color = vec4(1.0, 0.0, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let vs = vs::load(device.clone()).unwrap();
|
|
||||||
let fs = fs::load(device.clone()).unwrap();
|
|
||||||
|
|
||||||
let render_pass = vulkano::ordered_passes_renderpass!(
|
|
||||||
device.clone(),
|
|
||||||
attachments: {
|
|
||||||
color: {
|
|
||||||
load: Clear,
|
|
||||||
store: Store,
|
|
||||||
format: swapchain.image_format(),
|
|
||||||
samples: 1,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
passes: [
|
|
||||||
{ color: [color], depth_stencil: {}, input: [] },
|
|
||||||
{ color: [color], depth_stencil: {}, input: [] } // Create a second render-pass to draw egui
|
|
||||||
]
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let pipeline = GraphicsPipeline::start()
|
|
||||||
.vertex_input_state(BuffersDefinition::new().vertex::<Vertex>())
|
|
||||||
.vertex_shader(vs.entry_point("main").unwrap(), ())
|
|
||||||
.input_assembly_state(InputAssemblyState::new())
|
|
||||||
.viewport_state(ViewportState::viewport_dynamic_scissor_irrelevant())
|
|
||||||
.fragment_shader(fs.entry_point("main").unwrap(), ())
|
|
||||||
.render_pass(Subpass::from(render_pass.clone(), 0).unwrap())
|
|
||||||
.build(device.clone())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut viewport = Viewport {
|
|
||||||
origin: [0.0, 0.0],
|
|
||||||
dimensions: [0.0, 0.0],
|
|
||||||
depth_range: 0.0..1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut frame_buffers =
|
|
||||||
window_size_dependent_setup(&images, render_pass.clone(), &mut viewport);
|
|
||||||
|
|
||||||
let mut recreate_swapchain = false;
|
|
||||||
|
|
||||||
let mut previous_frame_end = Some(FrameEndFuture::now(device.clone()));
|
|
||||||
|
|
||||||
//Set up everything need to draw the gui
|
|
||||||
let window = surface.window();
|
|
||||||
let egui_ctx = egui::Context::default();
|
|
||||||
let mut egui_winit = egui_winit::State::new(4096, window);
|
|
||||||
|
|
||||||
let mut egui_painter = egui_vulkano::Painter::new(
|
|
||||||
device.clone(),
|
|
||||||
queue.clone(),
|
|
||||||
Subpass::from(render_pass.clone(), 1).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
//Set up some window to look at for the test
|
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
|
||||||
match event {
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: WindowEvent::CloseRequested,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
*control_flow = ControlFlow::Exit;
|
|
||||||
}
|
|
||||||
Event::WindowEvent {
|
|
||||||
event: WindowEvent::Resized(_),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
recreate_swapchain = true;
|
|
||||||
}
|
|
||||||
Event::WindowEvent { event, .. } => {
|
|
||||||
let egui_consumed_event = egui_winit.on_event(&egui_ctx, &event);
|
|
||||||
if !egui_consumed_event {
|
|
||||||
// do your own event handling here
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Event::RedrawEventsCleared => {
|
|
||||||
previous_frame_end
|
|
||||||
.as_mut()
|
|
||||||
.unwrap()
|
|
||||||
.as_mut()
|
|
||||||
.cleanup_finished();
|
|
||||||
|
|
||||||
if recreate_swapchain {
|
|
||||||
let dimensions: [u32; 2] = surface.window().inner_size().into();
|
|
||||||
let (new_swapchain, new_images) =
|
|
||||||
match swapchain.recreate(SwapchainCreateInfo {
|
|
||||||
image_extent: surface.window().inner_size().into(),
|
|
||||||
..swapchain.create_info()
|
|
||||||
}) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(SwapchainCreationError::ImageExtentNotSupported { .. }) => return,
|
|
||||||
Err(e) => panic!("Failed to recreate swapchain: {:?}", e),
|
|
||||||
};
|
|
||||||
|
|
||||||
swapchain = new_swapchain;
|
|
||||||
frame_buffers = window_size_dependent_setup(
|
|
||||||
&new_images,
|
|
||||||
render_pass.clone(),
|
|
||||||
&mut viewport,
|
|
||||||
);
|
|
||||||
viewport.dimensions = [dimensions[0] as f32, dimensions[1] as f32];
|
|
||||||
recreate_swapchain = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (image_num, suboptimal, acquire_future) =
|
|
||||||
match swapchain::acquire_next_image(swapchain.clone(), None) {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(AcquireError::OutOfDate) => {
|
|
||||||
recreate_swapchain = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("Failed to acquire next image: {:?}", e),
|
|
||||||
};
|
|
||||||
|
|
||||||
if suboptimal {
|
|
||||||
recreate_swapchain = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clear_values = vec![[0.0, 0.0, 1.0, 1.0].into()];
|
|
||||||
let mut builder = AutoCommandBufferBuilder::primary(
|
|
||||||
device.clone(),
|
|
||||||
queue.family(),
|
|
||||||
CommandBufferUsage::OneTimeSubmit,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
egui_ctx.begin_frame(egui_winit.take_egui_input(surface.window()));
|
|
||||||
|
|
||||||
create_ui(amd_gui.clone(), &egui_ctx);
|
|
||||||
|
|
||||||
let egui_output = egui_ctx.end_frame();
|
|
||||||
let platform_output = egui_output.platform_output;
|
|
||||||
egui_winit.handle_platform_output(surface.window(), &egui_ctx, platform_output);
|
|
||||||
|
|
||||||
let result = egui_painter
|
|
||||||
.update_textures(egui_output.textures_delta, &mut builder)
|
|
||||||
.expect("egui texture error");
|
|
||||||
|
|
||||||
let wait_for_last_frame = result == UpdateTexturesResult::Changed;
|
|
||||||
|
|
||||||
// Do your usual rendering
|
|
||||||
builder
|
|
||||||
.begin_render_pass(
|
|
||||||
frame_buffers[image_num].clone(),
|
|
||||||
SubpassContents::Inline,
|
|
||||||
clear_values,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.set_viewport(0, [viewport.clone()])
|
|
||||||
.bind_pipeline_graphics(pipeline.clone())
|
|
||||||
.bind_vertex_buffers(0, vertex_buffer.clone())
|
|
||||||
.draw(vertex_buffer.len().try_into().unwrap(), 1, 0, 0)
|
|
||||||
.unwrap(); // Don't end the render pass yet
|
|
||||||
|
|
||||||
// Build your gui
|
|
||||||
|
|
||||||
// Automatically start the next render subpass and draw the gui
|
|
||||||
let size = surface.window().inner_size();
|
|
||||||
let sf: f32 = surface.window().scale_factor() as f32;
|
|
||||||
egui_painter
|
|
||||||
.draw(
|
|
||||||
&mut builder,
|
|
||||||
[(size.width as f32) / sf, (size.height as f32) / sf],
|
|
||||||
&egui_ctx,
|
|
||||||
egui_output.shapes,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// End the render pass as usual
|
|
||||||
builder.end_render_pass().unwrap();
|
|
||||||
|
|
||||||
let command_buffer = builder.build().unwrap();
|
|
||||||
|
|
||||||
if wait_for_last_frame {
|
|
||||||
if let Some(FrameEndFuture::FenceSignalFuture(ref mut f)) = previous_frame_end {
|
|
||||||
f.wait(None).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let future = previous_frame_end
|
|
||||||
.take()
|
|
||||||
.unwrap()
|
|
||||||
.get()
|
|
||||||
.join(acquire_future)
|
|
||||||
.then_execute(queue.clone(), command_buffer)
|
|
||||||
.unwrap()
|
|
||||||
.then_swapchain_present(queue.clone(), swapchain.clone(), image_num)
|
|
||||||
.then_signal_fence_and_flush();
|
|
||||||
|
|
||||||
match future {
|
|
||||||
Ok(future) => {
|
|
||||||
previous_frame_end = Some(FrameEndFuture::FenceSignalFuture(future));
|
|
||||||
}
|
|
||||||
Err(FlushError::OutOfDate) => {
|
|
||||||
recreate_swapchain = true;
|
|
||||||
previous_frame_end = Some(FrameEndFuture::now(device.clone()));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("Failed to flush future: {:?}", e);
|
|
||||||
previous_frame_end = Some(FrameEndFuture::now(device.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn window_size_dependent_setup(
|
|
||||||
images: &[Arc<SwapchainImage<Window>>],
|
|
||||||
render_pass: Arc<RenderPass>,
|
|
||||||
viewport: &mut Viewport,
|
|
||||||
) -> Vec<Arc<Framebuffer>> {
|
|
||||||
let dimensions = images[0].dimensions().width_height();
|
|
||||||
viewport.dimensions = [dimensions[0] as f32, dimensions[1] as f32];
|
|
||||||
|
|
||||||
images
|
|
||||||
.iter()
|
|
||||||
.map(|image| {
|
|
||||||
let view = ImageView::new_default(image.clone()).unwrap();
|
|
||||||
Framebuffer::new(
|
|
||||||
render_pass.clone(),
|
|
||||||
FramebufferCreateInfo {
|
|
||||||
attachments: vec![view],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
//! Contains items that can be added to a plot.
|
|
||||||
|
|
||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
pub use arrows::*;
|
|
||||||
use egui::Pos2;
|
|
||||||
use epaint::{Color32, Shape, Stroke};
|
|
||||||
pub use h_line::*;
|
|
||||||
pub use line::*;
|
|
||||||
pub use marker_shape::*;
|
|
||||||
pub use plot_image::*;
|
|
||||||
pub use plot_item::*;
|
|
||||||
pub use points::*;
|
|
||||||
pub use polygons::*;
|
|
||||||
pub use text::*;
|
|
||||||
pub use v_line::*;
|
|
||||||
pub use value::Value;
|
|
||||||
pub use values::Values;
|
|
||||||
|
|
||||||
mod arrows;
|
|
||||||
mod h_line;
|
|
||||||
mod line;
|
|
||||||
mod marker_shape;
|
|
||||||
mod plot_image;
|
|
||||||
mod plot_item;
|
|
||||||
mod points;
|
|
||||||
mod polygons;
|
|
||||||
mod text;
|
|
||||||
mod v_line;
|
|
||||||
mod value;
|
|
||||||
mod values;
|
|
||||||
|
|
||||||
const DEFAULT_FILL_ALPHA: f32 = 0.05;
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
|
||||||
pub enum LineStyle {
|
|
||||||
Solid,
|
|
||||||
Dotted { spacing: f32 },
|
|
||||||
Dashed { length: f32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LineStyle {
|
|
||||||
pub fn dashed_loose() -> Self {
|
|
||||||
Self::Dashed { length: 10.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dashed_dense() -> Self {
|
|
||||||
Self::Dashed { length: 5.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dotted_loose() -> Self {
|
|
||||||
Self::Dotted { spacing: 10.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dotted_dense() -> Self {
|
|
||||||
Self::Dotted { spacing: 5.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_line(
|
|
||||||
&self,
|
|
||||||
line: Vec<Pos2>,
|
|
||||||
mut stroke: Stroke,
|
|
||||||
highlight: bool,
|
|
||||||
shapes: &mut Vec<Shape>,
|
|
||||||
) {
|
|
||||||
match line.len() {
|
|
||||||
0 => {}
|
|
||||||
1 => {
|
|
||||||
let mut radius = stroke.width / 2.0;
|
|
||||||
if highlight {
|
|
||||||
radius *= 2f32.sqrt();
|
|
||||||
}
|
|
||||||
shapes.push(Shape::circle_filled(line[0], radius, stroke.color));
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
match self {
|
|
||||||
LineStyle::Solid => {
|
|
||||||
if highlight {
|
|
||||||
stroke.width *= 2.0;
|
|
||||||
}
|
|
||||||
for point in line.iter() {
|
|
||||||
shapes.push(Shape::circle_filled(
|
|
||||||
*point,
|
|
||||||
stroke.width * 3.0,
|
|
||||||
Color32::DARK_BLUE,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
shapes.push(Shape::line(line, stroke));
|
|
||||||
}
|
|
||||||
LineStyle::Dotted { spacing } => {
|
|
||||||
let mut radius = stroke.width;
|
|
||||||
if highlight {
|
|
||||||
radius *= 2f32.sqrt();
|
|
||||||
}
|
|
||||||
shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius));
|
|
||||||
}
|
|
||||||
LineStyle::Dashed { length } => {
|
|
||||||
if highlight {
|
|
||||||
stroke.width *= 2.0;
|
|
||||||
}
|
|
||||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
|
||||||
shapes.extend(Shape::dashed_line(
|
|
||||||
&line,
|
|
||||||
stroke,
|
|
||||||
*length,
|
|
||||||
length * golden_ratio,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for LineStyle {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
LineStyle::Solid => "Solid".into(),
|
|
||||||
LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing),
|
|
||||||
LineStyle::Dashed { length } => format!("Dashed{}Px", length),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Describes a function y = f(x) with an optional range for x and a number of
|
|
||||||
/// points.
|
|
||||||
pub struct ExplicitGenerator {
|
|
||||||
function: Box<dyn Fn(f64) -> f64>,
|
|
||||||
x_range: RangeInclusive<f64>,
|
|
||||||
points: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Returns the x-coordinate of a possible intersection between a line segment
|
|
||||||
/// from `p1` to `p2` and a horizontal line at the given y-coordinate.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
|
|
||||||
((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y))
|
|
||||||
.then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::Ui;
|
|
||||||
use epaint::{Color32, Shape};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// A set of arrows.
|
|
||||||
pub struct Arrows {
|
|
||||||
pub(crate) origins: Values,
|
|
||||||
pub(crate) tips: Values,
|
|
||||||
pub(crate) color: Color32,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) highlight: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Arrows {
|
|
||||||
pub fn new(origins: Values, tips: Values) -> Self {
|
|
||||||
Self {
|
|
||||||
origins,
|
|
||||||
tips,
|
|
||||||
color: Color32::TRANSPARENT,
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for Arrows {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
use egui::emath::*;
|
|
||||||
use epaint::Stroke;
|
|
||||||
let Self {
|
|
||||||
origins,
|
|
||||||
tips,
|
|
||||||
color,
|
|
||||||
highlight,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color);
|
|
||||||
origins
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.zip(tips.values.iter())
|
|
||||||
.map(|(origin, tip)| {
|
|
||||||
(
|
|
||||||
transform.position_from_value(origin),
|
|
||||||
transform.position_from_value(tip),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.for_each(|(origin, tip)| {
|
|
||||||
let vector = tip - origin;
|
|
||||||
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
|
|
||||||
let tip_length = vector.length() / 4.0;
|
|
||||||
let tip = origin + vector;
|
|
||||||
let dir = vector.normalized();
|
|
||||||
shapes.push(Shape::line_segment([origin, tip], stroke));
|
|
||||||
shapes.push(Shape::line(
|
|
||||||
vec![
|
|
||||||
tip - tip_length * (rot.inverse() * dir),
|
|
||||||
tip,
|
|
||||||
tip - tip_length * (rot * dir),
|
|
||||||
],
|
|
||||||
stroke,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
|
|
||||||
self.origins
|
|
||||||
.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
|
|
||||||
self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
Some(&self.origins)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
self.origins.get_bounds()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::Ui;
|
|
||||||
use epaint::{Color32, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::items::LineStyle;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// A horizontal line in a plot, filling the full width
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct HLine {
|
|
||||||
pub(crate) y: f64,
|
|
||||||
pub(crate) stroke: Stroke,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) highlight: bool,
|
|
||||||
pub(crate) style: LineStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HLine {
|
|
||||||
pub fn new(y: impl Into<f64>) -> Self {
|
|
||||||
Self {
|
|
||||||
y: y.into(),
|
|
||||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
|
||||||
name: String::default(),
|
|
||||||
highlight: false,
|
|
||||||
style: LineStyle::Solid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
|
|
||||||
/// be auto-assigned.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.stroke.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for HLine {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let HLine {
|
|
||||||
y,
|
|
||||||
stroke,
|
|
||||||
highlight,
|
|
||||||
style,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
let points = vec![
|
|
||||||
transform.position_from_value(&Value::new(transform.bounds().min[0], *y)),
|
|
||||||
transform.position_from_value(&Value::new(transform.bounds().max[0], *y)),
|
|
||||||
];
|
|
||||||
style.style_line(points, *stroke, *highlight, shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.stroke.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
let mut bounds = Bounds::NOTHING;
|
|
||||||
bounds.min[1] = self.y;
|
|
||||||
bounds.max[1] = self.y;
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{pos2, NumExt, Ui};
|
|
||||||
use epaint::{Color32, Mesh, Rgba, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items;
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::items::{LineStyle, DEFAULT_FILL_ALPHA};
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
impl PlotItem for Line {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let Self {
|
|
||||||
series,
|
|
||||||
stroke,
|
|
||||||
highlight,
|
|
||||||
mut fill,
|
|
||||||
style,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let values_tf: Vec<_> = series
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|v| transform.position_from_value(v))
|
|
||||||
.collect();
|
|
||||||
let n_values = values_tf.len();
|
|
||||||
|
|
||||||
// Fill the area between the line and a reference line, if required.
|
|
||||||
if n_values < 2 {
|
|
||||||
fill = None;
|
|
||||||
}
|
|
||||||
if let Some(y_reference) = fill {
|
|
||||||
let mut fill_alpha = DEFAULT_FILL_ALPHA;
|
|
||||||
if *highlight {
|
|
||||||
fill_alpha = (2.0 * fill_alpha).at_most(1.0);
|
|
||||||
}
|
|
||||||
let y = transform
|
|
||||||
.position_from_value(&Value::new(0.0, y_reference))
|
|
||||||
.y;
|
|
||||||
let fill_color = Rgba::from(stroke.color)
|
|
||||||
.to_opaque()
|
|
||||||
.multiply(fill_alpha)
|
|
||||||
.into();
|
|
||||||
let mut mesh = Mesh::default();
|
|
||||||
let expected_intersections = 20;
|
|
||||||
mesh.reserve_triangles((n_values - 1) * 2);
|
|
||||||
mesh.reserve_vertices(n_values * 2 + expected_intersections);
|
|
||||||
values_tf[0..n_values - 1].windows(2).for_each(|w| {
|
|
||||||
let i = mesh.vertices.len() as u32;
|
|
||||||
mesh.colored_vertex(w[0], fill_color);
|
|
||||||
mesh.colored_vertex(pos2(w[0].x, y), fill_color);
|
|
||||||
if let Some(x) = items::y_intersection(&w[0], &w[1], y) {
|
|
||||||
let point = pos2(x, y);
|
|
||||||
mesh.colored_vertex(point, fill_color);
|
|
||||||
mesh.add_triangle(i, i + 1, i + 2);
|
|
||||||
mesh.add_triangle(i + 2, i + 3, i + 4);
|
|
||||||
} else {
|
|
||||||
mesh.add_triangle(i, i + 1, i + 2);
|
|
||||||
mesh.add_triangle(i + 1, i + 2, i + 3);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let last = values_tf[n_values - 1];
|
|
||||||
mesh.colored_vertex(last, fill_color);
|
|
||||||
mesh.colored_vertex(pos2(last.x, y), fill_color);
|
|
||||||
shapes.push(Shape::Mesh(mesh));
|
|
||||||
}
|
|
||||||
|
|
||||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
|
||||||
self.series.generate_points(x_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.stroke.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
Some(&self.series)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
self.series.get_bounds()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A series of values forming a path.
|
|
||||||
pub struct Line {
|
|
||||||
pub series: Values,
|
|
||||||
pub stroke: Stroke,
|
|
||||||
pub name: String,
|
|
||||||
pub highlight: bool,
|
|
||||||
pub fill: Option<f32>,
|
|
||||||
pub style: LineStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Line {
|
|
||||||
pub fn new(series: Values) -> Self {
|
|
||||||
Self {
|
|
||||||
series,
|
|
||||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
fill: None,
|
|
||||||
style: LineStyle::Solid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
|
|
||||||
/// be auto-assigned.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.stroke.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum MarkerShape {
|
|
||||||
Circle,
|
|
||||||
Diamond,
|
|
||||||
Square,
|
|
||||||
Cross,
|
|
||||||
Plus,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Asterisk,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MarkerShape {
|
|
||||||
/// Get a vector containing all marker shapes.
|
|
||||||
pub fn all() -> impl Iterator<Item = MarkerShape> {
|
|
||||||
[
|
|
||||||
Self::Circle,
|
|
||||||
Self::Diamond,
|
|
||||||
Self::Square,
|
|
||||||
Self::Cross,
|
|
||||||
Self::Plus,
|
|
||||||
Self::Up,
|
|
||||||
Self::Down,
|
|
||||||
Self::Left,
|
|
||||||
Self::Right,
|
|
||||||
Self::Asterisk,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{pos2, Image, Rect, Ui, Vec2};
|
|
||||||
use epaint::{Color32, Shape, Stroke, TextureId};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// An image in the plot.
|
|
||||||
pub struct PlotImage {
|
|
||||||
pub position: Value,
|
|
||||||
pub texture_id: TextureId,
|
|
||||||
pub uv: Rect,
|
|
||||||
pub size: Vec2,
|
|
||||||
pub bg_fill: Color32,
|
|
||||||
pub tint: Color32,
|
|
||||||
pub highlight: bool,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotImage {
|
|
||||||
/// Create a new image with position and size in plot coordinates.
|
|
||||||
pub fn new(texture_id: TextureId, position: Value, size: impl Into<Vec2>) -> Self {
|
|
||||||
Self {
|
|
||||||
position,
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
texture_id,
|
|
||||||
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
|
|
||||||
size: size.into(),
|
|
||||||
bg_fill: Default::default(),
|
|
||||||
tint: Color32::WHITE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight this image in the plot.
|
|
||||||
#[must_use]
|
|
||||||
pub fn highlight(mut self) -> Self {
|
|
||||||
self.highlight = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
|
|
||||||
#[must_use]
|
|
||||||
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
|
|
||||||
self.uv = uv.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A solid color to put behind the image. Useful for transparent images.
|
|
||||||
#[must_use]
|
|
||||||
pub fn bg_fill(mut self, bg_fill: impl Into<Color32>) -> Self {
|
|
||||||
self.bg_fill = bg_fill.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiply image color with this. Default is WHITE (no tint).
|
|
||||||
#[must_use]
|
|
||||||
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
|
|
||||||
self.tint = tint.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name of this image.
|
|
||||||
///
|
|
||||||
/// This name will show up in the plot legend, if legends are turned on.
|
|
||||||
///
|
|
||||||
/// Multiple plot items may share the same name, in which case they will
|
|
||||||
/// also share an entry in the legend.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl ToString) -> Self {
|
|
||||||
self.name = name.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for PlotImage {
|
|
||||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let Self {
|
|
||||||
position,
|
|
||||||
texture_id,
|
|
||||||
uv,
|
|
||||||
size,
|
|
||||||
bg_fill,
|
|
||||||
tint,
|
|
||||||
highlight,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
let rect = {
|
|
||||||
let left_top = Value::new(
|
|
||||||
position.x as f32 - size.x / 2.0,
|
|
||||||
position.y as f32 - size.y / 2.0,
|
|
||||||
);
|
|
||||||
let right_bottom = Value::new(
|
|
||||||
position.x as f32 + size.x / 2.0,
|
|
||||||
position.y as f32 + size.y / 2.0,
|
|
||||||
);
|
|
||||||
let left_top_tf = transform.position_from_value(&left_top);
|
|
||||||
let right_bottom_tf = transform.position_from_value(&right_bottom);
|
|
||||||
Rect::from_two_pos(left_top_tf, right_bottom_tf)
|
|
||||||
};
|
|
||||||
Image::new(*texture_id, *size)
|
|
||||||
.bg_fill(*bg_fill)
|
|
||||||
.tint(*tint)
|
|
||||||
.uv(*uv)
|
|
||||||
.paint_at(ui, rect);
|
|
||||||
if *highlight {
|
|
||||||
shapes.push(Shape::rect_stroke(
|
|
||||||
rect,
|
|
||||||
0.0,
|
|
||||||
Stroke::new(1.0, ui.visuals().strong_text_color()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
Color32::TRANSPARENT
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
let mut bounds = Bounds::NOTHING;
|
|
||||||
let left_top = Value::new(
|
|
||||||
self.position.x as f32 - self.size.x / 2.0,
|
|
||||||
self.position.y as f32 - self.size.y / 2.0,
|
|
||||||
);
|
|
||||||
let right_bottom = Value::new(
|
|
||||||
self.position.x as f32 + self.size.x / 2.0,
|
|
||||||
self.position.y as f32 + self.size.y / 2.0,
|
|
||||||
);
|
|
||||||
bounds.extend_with(&left_top);
|
|
||||||
bounds.extend_with(&right_bottom);
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::Ui;
|
|
||||||
use epaint::{Color32, Shape};
|
|
||||||
|
|
||||||
use crate::items::Values;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// Trait shared by things that can be drawn in the plot.
|
|
||||||
pub trait PlotItem {
|
|
||||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>);
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>);
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
fn color(&self) -> Color32;
|
|
||||||
fn highlight(&mut self);
|
|
||||||
fn highlighted(&self) -> bool;
|
|
||||||
fn values(&self) -> Option<&Values>;
|
|
||||||
fn get_bounds(&self) -> Bounds;
|
|
||||||
}
|
|
@ -1,250 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{pos2, vec2, Pos2, Ui};
|
|
||||||
use epaint::{Color32, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::marker_shape::MarkerShape;
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// A set of points.
|
|
||||||
pub struct Points {
|
|
||||||
pub(crate) series: Values,
|
|
||||||
pub(crate) shape: MarkerShape,
|
|
||||||
/// Color of the marker. `Color32::TRANSPARENT` means that it will be picked
|
|
||||||
/// automatically.
|
|
||||||
pub(crate) color: Color32,
|
|
||||||
/// Whether to fill the marker. Does not apply to all types.
|
|
||||||
pub(crate) filled: bool,
|
|
||||||
/// The maximum extent of the marker from its center.
|
|
||||||
pub(crate) radius: f32,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) highlight: bool,
|
|
||||||
pub(crate) stems: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Points {
|
|
||||||
pub fn new(series: Values) -> Self {
|
|
||||||
Self {
|
|
||||||
series,
|
|
||||||
shape: MarkerShape::Circle,
|
|
||||||
color: Color32::TRANSPARENT,
|
|
||||||
filled: true,
|
|
||||||
radius: 1.0,
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
stems: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the shape of the markers.
|
|
||||||
#[must_use]
|
|
||||||
pub fn shape(mut self, shape: MarkerShape) -> Self {
|
|
||||||
self.shape = shape;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight these points in the plot by scaling up their markers.
|
|
||||||
#[must_use]
|
|
||||||
pub fn highlight(mut self) -> Self {
|
|
||||||
self.highlight = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the marker's color.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to fill the marker.
|
|
||||||
#[must_use]
|
|
||||||
pub fn filled(mut self, filled: bool) -> Self {
|
|
||||||
self.filled = filled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to add stems between the markers and a horizontal reference
|
|
||||||
/// line.
|
|
||||||
#[must_use]
|
|
||||||
pub fn stems(mut self, y_reference: impl Into<f32>) -> Self {
|
|
||||||
self.stems = Some(y_reference.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the maximum extent of the marker around its position.
|
|
||||||
#[must_use]
|
|
||||||
pub fn radius(mut self, radius: impl Into<f32>) -> Self {
|
|
||||||
self.radius = radius.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name of this set of points.
|
|
||||||
///
|
|
||||||
/// This name will show up in the plot legend, if legends are turned on.
|
|
||||||
///
|
|
||||||
/// Multiple plot items may share the same name, in which case they will
|
|
||||||
/// also share an entry in the legend.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl ToString) -> Self {
|
|
||||||
self.name = name.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for Points {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let sqrt_3 = 3f32.sqrt();
|
|
||||||
let frac_sqrt_3_2 = 3f32.sqrt() / 2.0;
|
|
||||||
let frac_1_sqrt_2 = 1.0 / 2f32.sqrt();
|
|
||||||
|
|
||||||
let Self {
|
|
||||||
series,
|
|
||||||
shape,
|
|
||||||
color,
|
|
||||||
filled,
|
|
||||||
mut radius,
|
|
||||||
highlight,
|
|
||||||
stems,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let stroke_size = radius / 5.0;
|
|
||||||
|
|
||||||
let default_stroke = Stroke::new(stroke_size, *color);
|
|
||||||
let mut stem_stroke = default_stroke;
|
|
||||||
let stroke = (!filled)
|
|
||||||
.then_some(default_stroke)
|
|
||||||
.unwrap_or_else(Stroke::none);
|
|
||||||
let fill = filled.then(|| *color).unwrap_or_default();
|
|
||||||
|
|
||||||
if *highlight {
|
|
||||||
radius *= 2f32.sqrt();
|
|
||||||
stem_stroke.width *= 2.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let y_reference =
|
|
||||||
stems.map(|y| transform.position_from_value(&Value::new(0.0, y)).y as f32);
|
|
||||||
|
|
||||||
series
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|value| transform.position_from_value(value))
|
|
||||||
.for_each(|center| {
|
|
||||||
let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) };
|
|
||||||
|
|
||||||
if let Some(y) = y_reference {
|
|
||||||
let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke);
|
|
||||||
shapes.push(stem);
|
|
||||||
}
|
|
||||||
|
|
||||||
match shape {
|
|
||||||
MarkerShape::Circle => {
|
|
||||||
shapes.push(Shape::Circle(epaint::CircleShape {
|
|
||||||
center,
|
|
||||||
radius,
|
|
||||||
fill,
|
|
||||||
stroke,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
MarkerShape::Diamond => {
|
|
||||||
let points = vec![tf(1.0, 0.0), tf(0.0, -1.0), tf(-1.0, 0.0), tf(0.0, 1.0)];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Square => {
|
|
||||||
let points = vec![
|
|
||||||
tf(frac_1_sqrt_2, frac_1_sqrt_2),
|
|
||||||
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
|
|
||||||
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
|
|
||||||
tf(-frac_1_sqrt_2, frac_1_sqrt_2),
|
|
||||||
];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Cross => {
|
|
||||||
let diagonal1 = [
|
|
||||||
tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
|
|
||||||
tf(frac_1_sqrt_2, frac_1_sqrt_2),
|
|
||||||
];
|
|
||||||
let diagonal2 = [
|
|
||||||
tf(frac_1_sqrt_2, -frac_1_sqrt_2),
|
|
||||||
tf(-frac_1_sqrt_2, frac_1_sqrt_2),
|
|
||||||
];
|
|
||||||
shapes.push(Shape::line_segment(diagonal1, default_stroke));
|
|
||||||
shapes.push(Shape::line_segment(diagonal2, default_stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Plus => {
|
|
||||||
let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)];
|
|
||||||
let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
|
|
||||||
shapes.push(Shape::line_segment(horizontal, default_stroke));
|
|
||||||
shapes.push(Shape::line_segment(vertical, default_stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Up => {
|
|
||||||
let points =
|
|
||||||
vec![tf(0.0, -1.0), tf(-0.5 * sqrt_3, 0.5), tf(0.5 * sqrt_3, 0.5)];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Down => {
|
|
||||||
let points = vec![
|
|
||||||
tf(0.0, 1.0),
|
|
||||||
tf(-0.5 * sqrt_3, -0.5),
|
|
||||||
tf(0.5 * sqrt_3, -0.5),
|
|
||||||
];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Left => {
|
|
||||||
let points =
|
|
||||||
vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Right => {
|
|
||||||
let points = vec![
|
|
||||||
tf(1.0, 0.0),
|
|
||||||
tf(-0.5, -0.5 * sqrt_3),
|
|
||||||
tf(-0.5, 0.5 * sqrt_3),
|
|
||||||
];
|
|
||||||
shapes.push(Shape::convex_polygon(points, fill, stroke));
|
|
||||||
}
|
|
||||||
MarkerShape::Asterisk => {
|
|
||||||
let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
|
|
||||||
let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)];
|
|
||||||
let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)];
|
|
||||||
shapes.push(Shape::line_segment(vertical, default_stroke));
|
|
||||||
shapes.push(Shape::line_segment(diagonal1, default_stroke));
|
|
||||||
shapes.push(Shape::line_segment(diagonal2, default_stroke));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
|
||||||
self.series.generate_points(x_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
Some(&self.series)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
self.series.get_bounds()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{NumExt, Ui};
|
|
||||||
use epaint::{Color32, Rgba, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::items::{LineStyle, DEFAULT_FILL_ALPHA};
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// A convex polygon.
|
|
||||||
pub struct Polygon {
|
|
||||||
pub series: Values,
|
|
||||||
pub stroke: Stroke,
|
|
||||||
pub name: String,
|
|
||||||
pub highlight: bool,
|
|
||||||
pub fill_alpha: f32,
|
|
||||||
pub style: LineStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Polygon {
|
|
||||||
pub fn new(series: Values) -> Self {
|
|
||||||
Self {
|
|
||||||
series,
|
|
||||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
fill_alpha: DEFAULT_FILL_ALPHA,
|
|
||||||
style: LineStyle::Solid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight this polygon in the plot by scaling up the stroke and reducing
|
|
||||||
/// the fill transparency.
|
|
||||||
#[must_use]
|
|
||||||
pub fn highlight(mut self) -> Self {
|
|
||||||
self.highlight = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a custom stroke.
|
|
||||||
#[must_use]
|
|
||||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
|
||||||
self.stroke = stroke.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the stroke width.
|
|
||||||
#[must_use]
|
|
||||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
|
||||||
self.stroke.width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
|
|
||||||
/// be auto-assigned.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.stroke.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alpha of the filled area.
|
|
||||||
#[must_use]
|
|
||||||
pub fn fill_alpha(mut self, alpha: impl Into<f32>) -> Self {
|
|
||||||
self.fill_alpha = alpha.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the outline's style. Default is `LineStyle::Solid`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn style(mut self, style: LineStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name of this polygon.
|
|
||||||
///
|
|
||||||
/// This name will show up in the plot legend, if legends are turned on.
|
|
||||||
///
|
|
||||||
/// Multiple plot items may share the same name, in which case they will
|
|
||||||
/// also share an entry in the legend.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl ToString) -> Self {
|
|
||||||
self.name = name.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for Polygon {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let Self {
|
|
||||||
series,
|
|
||||||
stroke,
|
|
||||||
highlight,
|
|
||||||
mut fill_alpha,
|
|
||||||
style,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
if *highlight {
|
|
||||||
fill_alpha = (2.0 * fill_alpha).at_most(1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut values_tf: Vec<_> = series
|
|
||||||
.values
|
|
||||||
.iter()
|
|
||||||
.map(|v| transform.position_from_value(v))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha);
|
|
||||||
|
|
||||||
let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::none());
|
|
||||||
shapes.push(shape);
|
|
||||||
values_tf.push(*values_tf.first().unwrap());
|
|
||||||
style.style_line(values_tf, *stroke, *highlight, shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, x_range: RangeInclusive<f64>) {
|
|
||||||
self.series.generate_points(x_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.stroke.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
Some(&self.series)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
self.series.get_bounds()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,131 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{Align2, Rect, TextStyle, Ui};
|
|
||||||
use epaint::{Color32, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
/// Text inside the plot.
|
|
||||||
pub struct Text {
|
|
||||||
pub(crate) text: String,
|
|
||||||
pub(crate) style: TextStyle,
|
|
||||||
pub(crate) position: Value,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) highlight: bool,
|
|
||||||
pub(crate) color: Color32,
|
|
||||||
pub(crate) anchor: Align2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Text {
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
pub fn new(position: Value, text: impl ToString) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.to_string(),
|
|
||||||
style: TextStyle::Small,
|
|
||||||
position,
|
|
||||||
name: Default::default(),
|
|
||||||
highlight: false,
|
|
||||||
color: Color32::TRANSPARENT,
|
|
||||||
anchor: Align2::CENTER_CENTER,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight this text in the plot by drawing a rectangle around it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn highlight(mut self) -> Self {
|
|
||||||
self.highlight = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Text style. Default is `TextStyle::Small`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn style(mut self, style: TextStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Text color. Default is `Color32::TRANSPARENT` which means a color will
|
|
||||||
/// be auto-assigned.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Anchor position of the text. Default is `Align2::CENTER_CENTER`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn anchor(mut self, anchor: Align2) -> Self {
|
|
||||||
self.anchor = anchor;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name of this text.
|
|
||||||
///
|
|
||||||
/// This name will show up in the plot legend, if legends are turned on.
|
|
||||||
///
|
|
||||||
/// Multiple plot items may share the same name, in which case they will
|
|
||||||
/// also share an entry in the legend.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl ToString) -> Self {
|
|
||||||
self.name = name.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlotItem for Text {
|
|
||||||
fn get_shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let color = if self.color == Color32::TRANSPARENT {
|
|
||||||
ui.style().visuals.text_color()
|
|
||||||
} else {
|
|
||||||
self.color
|
|
||||||
};
|
|
||||||
let fond_id = ui.style().text_styles.get(&self.style).unwrap();
|
|
||||||
let pos = transform.position_from_value(&self.position);
|
|
||||||
let galley = ui
|
|
||||||
.fonts()
|
|
||||||
.layout_no_wrap(self.text.clone(), fond_id.clone(), color);
|
|
||||||
let rect = self
|
|
||||||
.anchor
|
|
||||||
.anchor_rect(Rect::from_min_size(pos, galley.size()));
|
|
||||||
shapes.push(Shape::galley(rect.min, galley));
|
|
||||||
if self.highlight {
|
|
||||||
shapes.push(Shape::rect_stroke(
|
|
||||||
rect.expand(2.0),
|
|
||||||
1.0,
|
|
||||||
Stroke::new(0.5, color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
let mut bounds = Bounds::NOTHING;
|
|
||||||
bounds.extend_with(&self.position);
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::Ui;
|
|
||||||
use epaint::{Color32, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::plot_item::PlotItem;
|
|
||||||
use crate::items::value::Value;
|
|
||||||
use crate::items::values::Values;
|
|
||||||
use crate::items::LineStyle;
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
|
|
||||||
impl PlotItem for VLine {
|
|
||||||
fn get_shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec<Shape>) {
|
|
||||||
let VLine {
|
|
||||||
x,
|
|
||||||
stroke,
|
|
||||||
highlight,
|
|
||||||
style,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
let points = vec![
|
|
||||||
transform.position_from_value(&Value::new(*x, transform.bounds().min[1])),
|
|
||||||
transform.position_from_value(&Value::new(*x, transform.bounds().max[1])),
|
|
||||||
];
|
|
||||||
style.style_line(points, *stroke, *highlight, shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color(&self) -> Color32 {
|
|
||||||
self.stroke.color
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlight(&mut self) {
|
|
||||||
self.highlight = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn highlighted(&self) -> bool {
|
|
||||||
self.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
fn values(&self) -> Option<&Values> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_bounds(&self) -> Bounds {
|
|
||||||
let mut bounds = Bounds::NOTHING;
|
|
||||||
bounds.min[0] = self.x;
|
|
||||||
bounds.max[0] = self.x;
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A vertical line in a plot, filling the full width
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct VLine {
|
|
||||||
pub(crate) x: f64,
|
|
||||||
pub(crate) stroke: Stroke,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) highlight: bool,
|
|
||||||
pub(crate) style: LineStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VLine {
|
|
||||||
pub fn new(x: impl Into<f64>) -> Self {
|
|
||||||
Self {
|
|
||||||
x: x.into(),
|
|
||||||
stroke: Stroke::new(1.0, Color32::TRANSPARENT),
|
|
||||||
name: String::default(),
|
|
||||||
highlight: false,
|
|
||||||
style: LineStyle::Solid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight this line in the plot by scaling up the line.
|
|
||||||
#[must_use]
|
|
||||||
pub fn highlight(mut self) -> Self {
|
|
||||||
self.highlight = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a stroke.
|
|
||||||
#[must_use]
|
|
||||||
pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
|
|
||||||
self.stroke = stroke.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke width. A high value means the plot thickens.
|
|
||||||
#[must_use]
|
|
||||||
pub fn width(mut self, width: impl Into<f32>) -> Self {
|
|
||||||
self.stroke.width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke color. Default is `Color32::TRANSPARENT` which means a color will
|
|
||||||
/// be auto-assigned.
|
|
||||||
#[must_use]
|
|
||||||
pub fn color(mut self, color: impl Into<Color32>) -> Self {
|
|
||||||
self.stroke.color = color.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the line's style. Default is `LineStyle::Solid`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn style(mut self, style: LineStyle) -> Self {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name of this vertical line.
|
|
||||||
///
|
|
||||||
/// This name will show up in the plot legend, if legends are turned on.
|
|
||||||
///
|
|
||||||
/// Multiple plot items may share the same name, in which case they will
|
|
||||||
/// also share an entry in the legend.
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
|
||||||
#[must_use]
|
|
||||||
pub fn name(mut self, name: impl ToString) -> Self {
|
|
||||||
self.name = name.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
/// A value in the value-space of the plot.
|
|
||||||
///
|
|
||||||
/// Uses f64 for improved accuracy to enable plotting
|
|
||||||
/// large values (e.g. unix time on x axis).
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Value {
|
|
||||||
/// This is often something monotonically increasing, such as time, but
|
|
||||||
/// doesn't have to be. Goes from left to right.
|
|
||||||
pub x: f64,
|
|
||||||
/// Goes from bottom to top (inverse of everything else in egui!).
|
|
||||||
pub y: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Value {
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Self {
|
|
||||||
Self {
|
|
||||||
x: x.into(),
|
|
||||||
y: y.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
use std::collections::Bound;
|
|
||||||
use std::ops::{RangeBounds, RangeInclusive};
|
|
||||||
|
|
||||||
use crate::items::{ExplicitGenerator, Value};
|
|
||||||
use crate::transform::Bounds;
|
|
||||||
|
|
||||||
pub struct Values {
|
|
||||||
pub values: Vec<Value>,
|
|
||||||
generator: Option<ExplicitGenerator>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Values {
|
|
||||||
pub fn from_values(values: Vec<Value>) -> Self {
|
|
||||||
Self {
|
|
||||||
values,
|
|
||||||
generator: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_values_iter(iter: impl Iterator<Item = Value>) -> Self {
|
|
||||||
Self::from_values(iter.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a line based on a function `y=f(x)`, a range (which can be
|
|
||||||
/// infinite) for x and the number of points.
|
|
||||||
pub fn from_explicit_callback(
|
|
||||||
function: impl Fn(f64) -> f64 + 'static,
|
|
||||||
x_range: impl RangeBounds<f64>,
|
|
||||||
points: usize,
|
|
||||||
) -> Self {
|
|
||||||
let start = match x_range.start_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
|
||||||
Bound::Unbounded => f64::NEG_INFINITY,
|
|
||||||
};
|
|
||||||
let end = match x_range.end_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => *x,
|
|
||||||
Bound::Unbounded => f64::INFINITY,
|
|
||||||
};
|
|
||||||
let x_range = start..=end;
|
|
||||||
|
|
||||||
let generator = ExplicitGenerator {
|
|
||||||
function: Box::new(function),
|
|
||||||
x_range,
|
|
||||||
points,
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
values: Vec::new(),
|
|
||||||
generator: Some(generator),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw a line based on a function `(x,y)=f(t)`, a range for t and the
|
|
||||||
/// number of points. The range may be specified as start..end or as
|
|
||||||
/// start..=end.
|
|
||||||
pub fn from_parametric_callback(
|
|
||||||
function: impl Fn(f64) -> (f64, f64),
|
|
||||||
t_range: impl RangeBounds<f64>,
|
|
||||||
points: usize,
|
|
||||||
) -> Self {
|
|
||||||
let start = match t_range.start_bound() {
|
|
||||||
Bound::Included(x) => x,
|
|
||||||
Bound::Excluded(_) => unreachable!(),
|
|
||||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
|
||||||
};
|
|
||||||
let end = match t_range.end_bound() {
|
|
||||||
Bound::Included(x) | Bound::Excluded(x) => x,
|
|
||||||
Bound::Unbounded => panic!("The range for parametric functions must be bounded!"),
|
|
||||||
};
|
|
||||||
let last_point_included = matches!(t_range.end_bound(), Bound::Included(_));
|
|
||||||
let increment = if last_point_included {
|
|
||||||
(end - start) / (points - 1) as f64
|
|
||||||
} else {
|
|
||||||
(end - start) / points as f64
|
|
||||||
};
|
|
||||||
let values = (0..points).map(|i| {
|
|
||||||
let t = start + i as f64 * increment;
|
|
||||||
let (x, y) = function(t);
|
|
||||||
Value { x, y }
|
|
||||||
});
|
|
||||||
Self::from_values_iter(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// From a series of y-values.
|
|
||||||
/// The x-values will be the indices of these values
|
|
||||||
pub fn from_ys_f32(ys: &[f32]) -> Self {
|
|
||||||
let values: Vec<Value> = ys
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, &y)| Value {
|
|
||||||
x: i as f64,
|
|
||||||
y: y as f64,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Self::from_values(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if there are no data points available and there is no
|
|
||||||
/// function to generate any.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.generator.is_none() && self.values.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If initialized with a generator function, this will generate `n` evenly
|
|
||||||
/// spaced points in the given range.
|
|
||||||
pub fn generate_points(&mut self, x_range: RangeInclusive<f64>) {
|
|
||||||
if let Some(generator) = self.generator.take() {
|
|
||||||
if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) {
|
|
||||||
let increment =
|
|
||||||
(intersection.end() - intersection.start()) / (generator.points - 1) as f64;
|
|
||||||
self.values = (0..generator.points)
|
|
||||||
.map(|i| {
|
|
||||||
let x = intersection.start() + i as f64 * increment;
|
|
||||||
let y = (generator.function)(x);
|
|
||||||
Value { x, y }
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the intersection of two ranges if they intersect.
|
|
||||||
fn range_intersection(
|
|
||||||
range1: &RangeInclusive<f64>,
|
|
||||||
range2: &RangeInclusive<f64>,
|
|
||||||
) -> Option<RangeInclusive<f64>> {
|
|
||||||
let start = range1.start().max(*range2.start());
|
|
||||||
let end = range1.end().min(*range2.end());
|
|
||||||
(start < end).then_some(start..=end)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_bounds(&self) -> Bounds {
|
|
||||||
let mut bounds = Bounds::NOTHING;
|
|
||||||
self.values
|
|
||||||
.iter()
|
|
||||||
.for_each(|value| bounds.extend_with(value));
|
|
||||||
bounds
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
use app::AmdGui;
|
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
|
||||||
|
|
||||||
mod app;
|
|
||||||
mod backend;
|
|
||||||
pub mod items;
|
|
||||||
pub mod transform;
|
|
||||||
pub mod widgets;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "DEBUG");
|
|
||||||
}
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
let config = Arc::new(Mutex::new(
|
|
||||||
amdgpu_config::fan::load_config(amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH)
|
|
||||||
.expect("No FAN config"),
|
|
||||||
));
|
|
||||||
let amd_gui = Arc::new(Mutex::new(AmdGui::new_with_config(config)));
|
|
||||||
|
|
||||||
let receiver = schedule_tick(amd_gui.clone());
|
|
||||||
|
|
||||||
backend::run_app(amd_gui, receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_tick(amd_gui: std::sync::Arc<parking_lot::Mutex<AmdGui>>) -> UnboundedReceiver<bool> {
|
|
||||||
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let sender = sender;
|
|
||||||
loop {
|
|
||||||
amd_gui.lock().tick();
|
|
||||||
if let Err(e) = sender.send(true) {
|
|
||||||
tracing::error!("Failed to propagate tick update. {:?}", e);
|
|
||||||
}
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(166)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
receiver
|
|
||||||
}
|
|
@ -1,244 +0,0 @@
|
|||||||
use std::ops::RangeInclusive;
|
|
||||||
|
|
||||||
use egui::{pos2, remap, Pos2, Rect, Vec2};
|
|
||||||
|
|
||||||
use crate::items::Value;
|
|
||||||
|
|
||||||
/// 2D bounding box of f64 precision.
|
|
||||||
/// The range of data values we show.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub struct Bounds {
|
|
||||||
pub min: [f64; 2],
|
|
||||||
pub max: [f64; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bounds {
|
|
||||||
pub const NOTHING: Self = Self {
|
|
||||||
min: [f64::INFINITY; 2],
|
|
||||||
max: [-f64::INFINITY; 2],
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn new_symmetrical(half_extent: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
min: [-half_extent; 2],
|
|
||||||
max: [half_extent; 2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_finite(&self) -> bool {
|
|
||||||
self.min[0].is_finite()
|
|
||||||
&& self.min[1].is_finite()
|
|
||||||
&& self.max[0].is_finite()
|
|
||||||
&& self.max[1].is_finite()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_valid(&self) -> bool {
|
|
||||||
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn width(&self) -> f64 {
|
|
||||||
self.max[0] - self.min[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn height(&self) -> f64 {
|
|
||||||
self.max[1] - self.min[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extend_with(&mut self, value: &Value) {
|
|
||||||
self.extend_with_x(value.x);
|
|
||||||
self.extend_with_y(value.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expand to include the given x coordinate
|
|
||||||
pub fn extend_with_x(&mut self, x: f64) {
|
|
||||||
self.min[0] = self.min[0].min(x);
|
|
||||||
self.max[0] = self.max[0].max(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expand to include the given y coordinate
|
|
||||||
pub fn extend_with_y(&mut self, y: f64) {
|
|
||||||
self.min[1] = self.min[1].min(y);
|
|
||||||
self.max[1] = self.max[1].max(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_x(&mut self, pad: f64) {
|
|
||||||
self.min[0] -= pad;
|
|
||||||
self.max[0] += pad;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expand_y(&mut self, pad: f64) {
|
|
||||||
self.min[1] -= pad;
|
|
||||||
self.max[1] += pad;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn merge(&mut self, other: &Bounds) {
|
|
||||||
self.min[0] = self.min[0].min(other.min[0]);
|
|
||||||
self.min[1] = self.min[1].min(other.min[1]);
|
|
||||||
self.max[0] = self.max[0].max(other.max[0]);
|
|
||||||
self.max[1] = self.max[1].max(other.max[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate_x(&mut self, delta: f64) {
|
|
||||||
self.min[0] += delta;
|
|
||||||
self.max[0] += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate_y(&mut self, delta: f64) {
|
|
||||||
self.min[1] += delta;
|
|
||||||
self.max[1] += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate(&mut self, delta: Vec2) {
|
|
||||||
self.translate_x(delta.x as f64);
|
|
||||||
self.translate_y(delta.y as f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_relative_margin(&mut self, margin_fraction: Vec2) {
|
|
||||||
let width = self.width().max(0.0);
|
|
||||||
let height = self.height().max(0.0);
|
|
||||||
self.expand_x(margin_fraction.x as f64 * width);
|
|
||||||
self.expand_y(margin_fraction.y as f64 * height);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn range_x(&self) -> RangeInclusive<f64> {
|
|
||||||
self.min[0]..=self.max[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_x_symmetrical(&mut self) {
|
|
||||||
let x_abs = self.min[0].abs().max(self.max[0].abs());
|
|
||||||
self.min[0] = -x_abs;
|
|
||||||
self.max[0] = x_abs;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_y_symmetrical(&mut self) {
|
|
||||||
let y_abs = self.min[1].abs().max(self.max[1].abs());
|
|
||||||
self.min[1] = -y_abs;
|
|
||||||
self.max[1] = y_abs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains the screen rectangle and the plot bounds and provides methods to
|
|
||||||
/// transform them.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ScreenTransform {
|
|
||||||
/// The screen rectangle.
|
|
||||||
frame: Rect,
|
|
||||||
/// The plot bounds.
|
|
||||||
bounds: Bounds,
|
|
||||||
/// Whether to always center the x-range of the bounds.
|
|
||||||
x_centered: bool,
|
|
||||||
/// Whether to always center the y-range of the bounds.
|
|
||||||
y_centered: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScreenTransform {
|
|
||||||
pub fn new(frame: Rect, bounds: Bounds, x_centered: bool, y_centered: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
frame,
|
|
||||||
bounds,
|
|
||||||
x_centered,
|
|
||||||
y_centered,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn frame(&self) -> &Rect {
|
|
||||||
&self.frame
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bounds(&self) -> &Bounds {
|
|
||||||
&self.bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate_bounds(&mut self, mut delta_pos: Vec2) {
|
|
||||||
if self.x_centered {
|
|
||||||
delta_pos.x = 0.;
|
|
||||||
}
|
|
||||||
if self.y_centered {
|
|
||||||
delta_pos.y = 0.;
|
|
||||||
}
|
|
||||||
delta_pos.x *= self.dvalue_dpos()[0] as f32;
|
|
||||||
delta_pos.y *= self.dvalue_dpos()[1] as f32;
|
|
||||||
self.bounds.translate(delta_pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zoom by a relative factor with the given screen position as center.
|
|
||||||
pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) {
|
|
||||||
let center = self.value_from_position(center);
|
|
||||||
|
|
||||||
let mut new_bounds = self.bounds;
|
|
||||||
new_bounds.min[0] = center.x + (new_bounds.min[0] - center.x) / (zoom_factor.x as f64);
|
|
||||||
new_bounds.max[0] = center.x + (new_bounds.max[0] - center.x) / (zoom_factor.x as f64);
|
|
||||||
new_bounds.min[1] = center.y + (new_bounds.min[1] - center.y) / (zoom_factor.y as f64);
|
|
||||||
new_bounds.max[1] = center.y + (new_bounds.max[1] - center.y) / (zoom_factor.y as f64);
|
|
||||||
|
|
||||||
if new_bounds.is_valid() {
|
|
||||||
self.bounds = new_bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn position_from_value(&self, value: &Value) -> Pos2 {
|
|
||||||
let x = remap(
|
|
||||||
value.x,
|
|
||||||
self.bounds.min[0]..=self.bounds.max[0],
|
|
||||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
|
||||||
);
|
|
||||||
let y = remap(
|
|
||||||
value.y,
|
|
||||||
self.bounds.min[1]..=self.bounds.max[1],
|
|
||||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
|
||||||
);
|
|
||||||
pos2(x as f32, y as f32)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value_from_position(&self, pos: Pos2) -> Value {
|
|
||||||
let x = remap(
|
|
||||||
pos.x as f64,
|
|
||||||
(self.frame.left() as f64)..=(self.frame.right() as f64),
|
|
||||||
self.bounds.min[0]..=self.bounds.max[0],
|
|
||||||
);
|
|
||||||
let y = remap(
|
|
||||||
pos.y as f64,
|
|
||||||
(self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis!
|
|
||||||
self.bounds.min[1]..=self.bounds.max[1],
|
|
||||||
);
|
|
||||||
Value::new(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delta position / delta value
|
|
||||||
pub fn dpos_dvalue_x(&self) -> f64 {
|
|
||||||
self.frame.width() as f64 / self.bounds.width()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delta position / delta value
|
|
||||||
pub fn dpos_dvalue_y(&self) -> f64 {
|
|
||||||
-self.frame.height() as f64 / self.bounds.height() // negated y axis!
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delta position / delta value
|
|
||||||
pub fn dpos_dvalue(&self) -> [f64; 2] {
|
|
||||||
[self.dpos_dvalue_x(), self.dpos_dvalue_y()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// delta value / delta position
|
|
||||||
pub fn dvalue_dpos(&self) -> [f64; 2] {
|
|
||||||
[1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_aspect(&self) -> f64 {
|
|
||||||
let rw = self.frame.width() as f64;
|
|
||||||
let rh = self.frame.height() as f64;
|
|
||||||
(self.bounds.width() / rw) / (self.bounds.height() / rh)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_aspect(&mut self, aspect: f64) {
|
|
||||||
let epsilon = 1e-5;
|
|
||||||
let current_aspect = self.get_aspect();
|
|
||||||
if current_aspect < aspect - epsilon {
|
|
||||||
self.bounds
|
|
||||||
.expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5);
|
|
||||||
} else if current_aspect > aspect + epsilon {
|
|
||||||
self.bounds
|
|
||||||
.expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
use amdgpu::pidfile::helper_cmd::{send_command, Command, Response};
|
|
||||||
use amdgpu_config::fan::MatrixPoint;
|
|
||||||
use egui::{emath, pos2, Layout, PointerButton, Ui};
|
|
||||||
use epaint::Color32;
|
|
||||||
|
|
||||||
use crate::app::{ChangeState, FanConfig, FanServices, StatefulConfig};
|
|
||||||
use crate::widgets;
|
|
||||||
use crate::widgets::drag_plot::PlotMsg;
|
|
||||||
use crate::widgets::reload_section::ReloadSection;
|
|
||||||
use crate::widgets::ConfigFile;
|
|
||||||
|
|
||||||
pub struct ChangeFanSettings {
|
|
||||||
config: FanConfig,
|
|
||||||
selected: Option<usize>,
|
|
||||||
matrix: Vec<MatrixPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ChangeFanSettings {
|
|
||||||
pub fn new(config: FanConfig) -> Self {
|
|
||||||
let matrix = config.lock().speed_matrix().to_vec();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
matrix,
|
|
||||||
config,
|
|
||||||
selected: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select(&mut self, idx: usize) {
|
|
||||||
self.selected = Some(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deselect(&mut self) {
|
|
||||||
self.selected = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&mut self, ui: &mut Ui, pid_files: &mut FanServices, state: &mut StatefulConfig) {
|
|
||||||
let available = ui.available_rect_before_wrap();
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
ui.child_ui(
|
|
||||||
emath::Rect {
|
|
||||||
min: available.min,
|
|
||||||
max: pos2(available.width() / 2.0, available.height()),
|
|
||||||
},
|
|
||||||
Layout::left_to_right(),
|
|
||||||
)
|
|
||||||
.vertical(|ui| {
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.enable_scrolling(true)
|
|
||||||
.id_source("plot-and-reload")
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.add({
|
|
||||||
let curve = {
|
|
||||||
let config = self.config.lock();
|
|
||||||
let iter = config
|
|
||||||
.speed_matrix()
|
|
||||||
.iter()
|
|
||||||
.map(|v| crate::items::Value::new(v.temp, v.speed));
|
|
||||||
crate::items::Line::new(crate::items::Values::from_values_iter(
|
|
||||||
iter,
|
|
||||||
))
|
|
||||||
.color(Color32::BLUE)
|
|
||||||
};
|
|
||||||
widgets::drag_plot::DragPlot::new("change fan settings")
|
|
||||||
.height(600.0)
|
|
||||||
.width(available.width() / 2.0)
|
|
||||||
.selected(self.selected)
|
|
||||||
.allow_drag(true)
|
|
||||||
.allow_zoom(false)
|
|
||||||
.line(curve)
|
|
||||||
.y_axis_name("Speed")
|
|
||||||
.x_axis_name("Temperature")
|
|
||||||
.hline(crate::items::HLine::new(0.0).color(Color32::BLACK))
|
|
||||||
.hline(crate::items::HLine::new(100.0).color(Color32::BLACK))
|
|
||||||
.vline(crate::items::VLine::new(0.0).color(Color32::BLACK))
|
|
||||||
.vline(crate::items::VLine::new(100.0).color(Color32::BLACK))
|
|
||||||
.on_event(|msg| match msg {
|
|
||||||
PlotMsg::Clicked(idx) => {
|
|
||||||
self.selected = Some(idx);
|
|
||||||
}
|
|
||||||
PlotMsg::Drag(delta) => {
|
|
||||||
if let Some(idx) = self.selected {
|
|
||||||
let mut config = self.config.lock();
|
|
||||||
let min = idx
|
|
||||||
.checked_sub(1)
|
|
||||||
.and_then(|i| config.speed_matrix().get(i).copied())
|
|
||||||
.unwrap_or(MatrixPoint::MIN);
|
|
||||||
let max = idx
|
|
||||||
.checked_add(1)
|
|
||||||
.and_then(|i| config.speed_matrix().get(i).copied())
|
|
||||||
.unwrap_or(MatrixPoint::MAX);
|
|
||||||
let current = config.speed_matrix_mut().get_mut(idx);
|
|
||||||
if let Some((cache, current)) =
|
|
||||||
self.matrix.get_mut(idx).zip(current.as_deref())
|
|
||||||
{
|
|
||||||
cache.speed = current.speed;
|
|
||||||
cache.temp = current.temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(point) = current {
|
|
||||||
point.speed = (point.speed + delta.y as f64)
|
|
||||||
.max(min.speed)
|
|
||||||
.min(max.speed);
|
|
||||||
point.temp = (point.temp + delta.x as f64)
|
|
||||||
.max(min.temp)
|
|
||||||
.min(max.temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.legend(widgets::legend::Legend::default())
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
Self::save_button(self.config.clone(), state, ui);
|
|
||||||
ui.add(ReloadSection::new(pid_files));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.child_ui(
|
|
||||||
emath::Rect {
|
|
||||||
min: pos2(available.width() / 2.0 + 20.0, available.min.y),
|
|
||||||
max: available.max,
|
|
||||||
},
|
|
||||||
Layout::left_to_right(),
|
|
||||||
)
|
|
||||||
.vertical(|ui| {
|
|
||||||
ui.add(ConfigFile::new(self.config.clone(), &mut self.matrix));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_button(config: FanConfig, state: &mut StatefulConfig, ui: &mut Ui) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui.button("Save").clicked_by(PointerButton::Primary) {
|
|
||||||
Self::save_config(config, state);
|
|
||||||
}
|
|
||||||
match &state.state {
|
|
||||||
ChangeState::New => {}
|
|
||||||
ChangeState::Reloading => {
|
|
||||||
ui.label("Saving...");
|
|
||||||
}
|
|
||||||
ChangeState::Success => {
|
|
||||||
ui.add(egui::Label::new("Saved")/*.text_color(Color32::GREEN)*/);
|
|
||||||
}
|
|
||||||
ChangeState::Failure(msg) => {
|
|
||||||
ui.add(egui::Label::new(format!("Failure. {}", msg))/*.text_color(Color32::RED)*/);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::explicit_auto_deref)]
|
|
||||||
fn save_config(config: FanConfig, state: &mut StatefulConfig) {
|
|
||||||
state.state = ChangeState::Reloading;
|
|
||||||
|
|
||||||
let config = config.lock();
|
|
||||||
|
|
||||||
let c: &amdgpu_config::fan::Config = &*config;
|
|
||||||
let content = match toml::to_string(c) {
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Config file serialization failed. {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok(content) => content,
|
|
||||||
};
|
|
||||||
let command = Command::SaveFanConfig {
|
|
||||||
path: String::from(config.path()),
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
match send_command(command) {
|
|
||||||
Ok(Response::ConfigFileSaveFailed(msg)) => {
|
|
||||||
state.state = ChangeState::Failure(msg);
|
|
||||||
}
|
|
||||||
Ok(Response::ConfigFileSaved) => {
|
|
||||||
state.state = ChangeState::Success;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
use amdgpu_config::fan::MatrixPoint;
|
|
||||||
use egui::{PointerButton, Response, Sense, Ui, Widget};
|
|
||||||
|
|
||||||
use crate::app::FanConfig;
|
|
||||||
|
|
||||||
pub struct ConfigFile<'l> {
|
|
||||||
config: FanConfig,
|
|
||||||
matrix: &'l mut [MatrixPoint],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'l> ConfigFile<'l> {
|
|
||||||
pub fn new(config: FanConfig, matrix: &'l mut [MatrixPoint]) -> Self {
|
|
||||||
Self { config, matrix }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'l> Widget for ConfigFile<'l> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let config = self.config.clone();
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let mut matrix = self.matrix.iter_mut().enumerate().peekable();
|
|
||||||
|
|
||||||
let mut prev: Option<MatrixPoint> = None;
|
|
||||||
|
|
||||||
while let Some((idx, current)) = matrix.next() {
|
|
||||||
let min: MatrixPoint = if current == &MatrixPoint::MIN {
|
|
||||||
MatrixPoint::MIN
|
|
||||||
} else if let Some(prev) = &prev {
|
|
||||||
*prev
|
|
||||||
} else {
|
|
||||||
MatrixPoint::MIN
|
|
||||||
};
|
|
||||||
let next = matrix.peek();
|
|
||||||
let max: MatrixPoint = if current == &MatrixPoint::MAX {
|
|
||||||
MatrixPoint::MAX
|
|
||||||
} else if let Some(next) = next.map(|(_, n)| n) {
|
|
||||||
MatrixPoint::new(next.temp, next.speed)
|
|
||||||
} else {
|
|
||||||
MatrixPoint::MAX
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
ui.label("Speed");
|
|
||||||
if ui
|
|
||||||
.add(egui::Slider::new(&mut current.speed, min.speed..=max.speed))
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
if let Some(entry) = config.lock().speed_matrix_mut().get_mut(idx) {
|
|
||||||
entry.speed = current.speed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
ui.label("Temperature");
|
|
||||||
if ui
|
|
||||||
.add(egui::Slider::new(&mut current.temp, min.temp..=max.temp))
|
|
||||||
.changed()
|
|
||||||
{
|
|
||||||
if let Some(entry) = config.lock().speed_matrix_mut().get_mut(idx) {
|
|
||||||
entry.temp = current.temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if next.is_some() {
|
|
||||||
if ui
|
|
||||||
.add(egui::Button::new("Add in the middle"))
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
config.lock().speed_matrix_vec_mut().insert(
|
|
||||||
idx + 1,
|
|
||||||
MatrixPoint::new(
|
|
||||||
min.speed + ((max.speed - min.speed) / 2.0),
|
|
||||||
min.temp + ((max.temp - min.temp) / 2.0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if next.is_none()
|
|
||||||
&& *current != MatrixPoint::MAX
|
|
||||||
&& ui
|
|
||||||
.add(egui::Button::new("Add"))
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
config
|
|
||||||
.lock()
|
|
||||||
.speed_matrix_vec_mut()
|
|
||||||
.push(MatrixPoint::new(100.0, 100.0))
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add(egui::Button::new("Remove"))
|
|
||||||
.clicked_by(PointerButton::Primary)
|
|
||||||
{
|
|
||||||
config.lock().speed_matrix_vec_mut().remove(idx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
prev = Some(*current);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.allocate_response(ui.available_size(), Sense::click())
|
|
||||||
})
|
|
||||||
.inner
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
use core::option::Option;
|
|
||||||
use core::option::Option::Some;
|
|
||||||
use std::collections::vec_deque::VecDeque;
|
|
||||||
|
|
||||||
use amdgpu::Card;
|
|
||||||
use amdmond_lib::AmdMon;
|
|
||||||
use egui::Ui;
|
|
||||||
|
|
||||||
use crate::app::{FanConfig, FanServices};
|
|
||||||
|
|
||||||
pub struct CoolingPerformance {
|
|
||||||
capacity: usize,
|
|
||||||
data: VecDeque<f64>,
|
|
||||||
amd_mon: Option<AmdMon>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoolingPerformance {
|
|
||||||
#[allow(clippy::explicit_auto_deref)]
|
|
||||||
pub fn new(capacity: usize, fan_config: FanConfig) -> Self {
|
|
||||||
let amd_mon = amdgpu::hw_mon::open_hw_mon(Card(0))
|
|
||||||
.map(|hw| amdmond_lib::AmdMon::wrap(hw, &*fan_config.lock()))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
capacity,
|
|
||||||
data: VecDeque::with_capacity(capacity),
|
|
||||||
amd_mon,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
|
||||||
if let Some(temp) = self
|
|
||||||
.amd_mon
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|mon| mon.gpu_temp_of(0))
|
|
||||||
.and_then(|(_, value)| value.ok())
|
|
||||||
{
|
|
||||||
self.push(temp as f64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&self, ui: &mut Ui, pid_files: &FanServices) {
|
|
||||||
use egui::widgets::plot::*;
|
|
||||||
use epaint::color::Color32;
|
|
||||||
|
|
||||||
let current = self.data.iter().last().copied().unwrap_or_default();
|
|
||||||
|
|
||||||
let iter = self
|
|
||||||
.data
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, v)| Value::new(i as f64, *v));
|
|
||||||
|
|
||||||
let curve = Line::new(Values::from_values_iter(iter)).color(Color32::BLUE);
|
|
||||||
let zero = HLine::new(0.0).color(Color32::from_white_alpha(0));
|
|
||||||
let optimal = HLine::new(45.0).name("Optimal").color(Color32::LIGHT_BLUE);
|
|
||||||
let target = HLine::new(80.0)
|
|
||||||
.name("Overheating")
|
|
||||||
.color(Color32::DARK_RED);
|
|
||||||
|
|
||||||
ui.label("Temperature");
|
|
||||||
Plot::new("cooling performance")
|
|
||||||
.allow_drag(false)
|
|
||||||
.allow_zoom(false)
|
|
||||||
.height(600.0)
|
|
||||||
.show(ui, |plot_ui| {
|
|
||||||
plot_ui.line(curve);
|
|
||||||
plot_ui.hline(zero);
|
|
||||||
plot_ui.hline(optimal);
|
|
||||||
plot_ui.hline(target);
|
|
||||||
// plot_ui.legend(Legend::default());
|
|
||||||
});
|
|
||||||
|
|
||||||
// ui.add(plot);
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Current temperature");
|
|
||||||
ui.label(format!("{:<3.2}°C", current));
|
|
||||||
});
|
|
||||||
ui.label("Working services");
|
|
||||||
if pid_files.0.is_empty() {
|
|
||||||
ui.label(" There's no working services");
|
|
||||||
} else {
|
|
||||||
pid_files.0.iter().for_each(|service| {
|
|
||||||
ui.label(format!(" {}", service.pid.0));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(&mut self, v: f64) {
|
|
||||||
if self.data.len() >= self.capacity {
|
|
||||||
self.data.pop_front();
|
|
||||||
}
|
|
||||||
self.data.push_back(v);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,441 +0,0 @@
|
|||||||
use egui::{emath, vec2, CursorIcon, Id, NumExt, PointerButton, Response, Sense, Ui, Vec2};
|
|
||||||
use epaint::ahash::AHashSet;
|
|
||||||
use epaint::color::Hsva;
|
|
||||||
use epaint::{Color32, Rounding};
|
|
||||||
|
|
||||||
use crate::items::{HLine, *};
|
|
||||||
use crate::transform::{Bounds, ScreenTransform};
|
|
||||||
use crate::widgets::drag_plot_prepared::DragPlotPrepared;
|
|
||||||
use crate::widgets::legend::Legend;
|
|
||||||
use crate::widgets::legend_widget::LegendWidget;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum PlotMsg {
|
|
||||||
Clicked(usize),
|
|
||||||
Drag(emath::Vec2),
|
|
||||||
}
|
|
||||||
|
|
||||||
// , serde::Serialize, serde::Deserialize
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct PlotMemory {
|
|
||||||
bounds: Bounds,
|
|
||||||
auto_bounds: bool,
|
|
||||||
hovered_entry: Option<String>,
|
|
||||||
hidden_items: AHashSet<String>,
|
|
||||||
min_auto_bounds: Bounds,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DragPlot<OnEvent>
|
|
||||||
where
|
|
||||||
OnEvent: FnMut(PlotMsg),
|
|
||||||
{
|
|
||||||
id: egui::Id,
|
|
||||||
|
|
||||||
items: Vec<Box<dyn PlotItem>>,
|
|
||||||
|
|
||||||
min_auto_bounds: Bounds,
|
|
||||||
min_size: Vec2,
|
|
||||||
width: Option<f32>,
|
|
||||||
height: Option<f32>,
|
|
||||||
data_aspect: Option<f32>,
|
|
||||||
view_aspect: Option<f32>,
|
|
||||||
legend_config: Option<Legend>,
|
|
||||||
|
|
||||||
next_auto_color_idx: usize,
|
|
||||||
allow_zoom: bool,
|
|
||||||
allow_drag: bool,
|
|
||||||
margin_fraction: Vec2,
|
|
||||||
selected: Option<usize>,
|
|
||||||
on_event: Option<OnEvent>,
|
|
||||||
|
|
||||||
lines: Vec<Line>,
|
|
||||||
axis_names: [String; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<OnEvent> DragPlot<OnEvent>
|
|
||||||
where
|
|
||||||
OnEvent: FnMut(PlotMsg),
|
|
||||||
{
|
|
||||||
pub fn new(id_source: impl std::hash::Hash) -> Self {
|
|
||||||
Self {
|
|
||||||
id: Id::new(id_source),
|
|
||||||
|
|
||||||
items: Default::default(),
|
|
||||||
min_size: Vec2::splat(64.0),
|
|
||||||
width: None,
|
|
||||||
height: None,
|
|
||||||
data_aspect: None,
|
|
||||||
view_aspect: None,
|
|
||||||
min_auto_bounds: Bounds::NOTHING,
|
|
||||||
legend_config: None,
|
|
||||||
next_auto_color_idx: 0,
|
|
||||||
allow_zoom: true,
|
|
||||||
allow_drag: true,
|
|
||||||
margin_fraction: Vec2::splat(0.05),
|
|
||||||
selected: None,
|
|
||||||
on_event: None,
|
|
||||||
lines: vec![],
|
|
||||||
axis_names: [String::from("x"), String::from("y")],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn x_axis_name<S: Into<String>>(mut self, name: S) -> Self {
|
|
||||||
self.axis_names[0] = name.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn y_axis_name<S: Into<String>>(mut self, name: S) -> Self {
|
|
||||||
self.axis_names[1] = name.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show a legend including all named items.
|
|
||||||
#[must_use]
|
|
||||||
pub fn legend(mut self, legend: Legend) -> Self {
|
|
||||||
self.legend_config = Some(legend);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a data lines.
|
|
||||||
#[must_use]
|
|
||||||
pub fn line(mut self, mut line: Line) -> Self {
|
|
||||||
if line.series.is_empty() {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Give the stroke an automatic color if no color has been assigned.
|
|
||||||
if line.stroke.color == Color32::TRANSPARENT {
|
|
||||||
line.stroke.color = self.auto_color();
|
|
||||||
}
|
|
||||||
self.lines.push(line);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn selected(mut self, selected: Option<usize>) -> Self {
|
|
||||||
self.selected = selected;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn auto_color(&mut self) -> Color32 {
|
|
||||||
let i = self.next_auto_color_idx;
|
|
||||||
self.next_auto_color_idx += 1;
|
|
||||||
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
|
|
||||||
let h = i as f32 * golden_ratio;
|
|
||||||
Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO: OkLab or some other
|
|
||||||
// perspective color space
|
|
||||||
}
|
|
||||||
|
|
||||||
/// width / height ratio of the data.
|
|
||||||
/// For instance, it can be useful to set this to `1.0` for when the two
|
|
||||||
/// axes show the same unit.
|
|
||||||
/// By default the plot window's aspect ratio is used.
|
|
||||||
#[must_use]
|
|
||||||
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
|
|
||||||
self.data_aspect = Some(data_aspect);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// width / height ratio of the plot region.
|
|
||||||
/// By default no fixed aspect ratio is set (and width/height will fill the
|
|
||||||
/// ui it is in).
|
|
||||||
#[must_use]
|
|
||||||
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
|
|
||||||
self.view_aspect = Some(view_aspect);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Width of plot. By default a plot will fill the ui it is in.
|
|
||||||
/// If you set [`Self::view_aspect`], the width can be calculated from the
|
|
||||||
/// height.
|
|
||||||
#[must_use]
|
|
||||||
pub fn width(mut self, width: f32) -> Self {
|
|
||||||
self.min_size.x = width;
|
|
||||||
self.width = Some(width);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Height of plot. By default a plot will fill the ui it is in.
|
|
||||||
/// If you set [`Self::view_aspect`], the height can be calculated from the
|
|
||||||
/// width.
|
|
||||||
#[must_use]
|
|
||||||
pub fn height(mut self, height: f32) -> Self {
|
|
||||||
self.min_size.y = height;
|
|
||||||
self.height = Some(height);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimum size of the plot view.
|
|
||||||
#[must_use]
|
|
||||||
pub fn min_size(mut self, min_size: Vec2) -> Self {
|
|
||||||
self.min_size = min_size;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn allow_drag(mut self, allow_drag: bool) -> Self {
|
|
||||||
self.allow_drag = allow_drag;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn allow_zoom(mut self, allow_zoom: bool) -> Self {
|
|
||||||
self.allow_zoom = allow_zoom;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a horizontal line.
|
|
||||||
/// Can be useful e.g. to show min/max bounds or similar.
|
|
||||||
/// Always fills the full width of the plot.
|
|
||||||
#[must_use]
|
|
||||||
pub fn hline(mut self, mut hline: HLine) -> Self {
|
|
||||||
if hline.stroke.color == Color32::TRANSPARENT {
|
|
||||||
hline.stroke.color = self.auto_color();
|
|
||||||
}
|
|
||||||
self.items.push(Box::new(hline));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a vertical line.
|
|
||||||
/// Can be useful e.g. to show min/max bounds or similar.
|
|
||||||
/// Always fills the full height of the plot.
|
|
||||||
#[must_use]
|
|
||||||
pub fn vline(mut self, mut vline: VLine) -> Self {
|
|
||||||
if vline.stroke.color == Color32::TRANSPARENT {
|
|
||||||
vline.stroke.color = self.auto_color();
|
|
||||||
}
|
|
||||||
self.items.push(Box::new(vline));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn on_event(mut self, f: OnEvent) -> Self {
|
|
||||||
self.on_event = Some(f);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<OnEvent> egui::Widget for DragPlot<OnEvent>
|
|
||||||
where
|
|
||||||
OnEvent: FnMut(PlotMsg),
|
|
||||||
{
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let Self {
|
|
||||||
id,
|
|
||||||
mut items,
|
|
||||||
min_auto_bounds,
|
|
||||||
min_size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
data_aspect,
|
|
||||||
view_aspect,
|
|
||||||
legend_config,
|
|
||||||
next_auto_color_idx: _,
|
|
||||||
allow_zoom,
|
|
||||||
allow_drag,
|
|
||||||
margin_fraction,
|
|
||||||
selected: _,
|
|
||||||
on_event,
|
|
||||||
mut lines,
|
|
||||||
axis_names,
|
|
||||||
} = self;
|
|
||||||
let plot_id = ui.make_persistent_id(id);
|
|
||||||
let memory = ui
|
|
||||||
.ctx()
|
|
||||||
.data()
|
|
||||||
.get_persisted_mut_or_insert_with(plot_id, || PlotMemory {
|
|
||||||
bounds: min_auto_bounds,
|
|
||||||
auto_bounds: false,
|
|
||||||
hovered_entry: None,
|
|
||||||
hidden_items: Default::default(),
|
|
||||||
min_auto_bounds,
|
|
||||||
})
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let PlotMemory {
|
|
||||||
mut bounds,
|
|
||||||
mut auto_bounds,
|
|
||||||
mut hovered_entry,
|
|
||||||
mut hidden_items,
|
|
||||||
min_auto_bounds,
|
|
||||||
} = memory;
|
|
||||||
|
|
||||||
// Determine the size of the plot in the UI
|
|
||||||
let size = {
|
|
||||||
let width = width
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
if let (Some(height), Some(aspect)) = (height, view_aspect) {
|
|
||||||
height * aspect
|
|
||||||
} else {
|
|
||||||
ui.available_size_before_wrap().x
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.at_least(min_size.x);
|
|
||||||
|
|
||||||
let height = height
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
if let Some(aspect) = view_aspect {
|
|
||||||
width / aspect
|
|
||||||
} else {
|
|
||||||
ui.available_size_before_wrap().y
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.at_least(min_size.y);
|
|
||||||
vec2(width, height)
|
|
||||||
};
|
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_exact_size(size, Sense::click_and_drag());
|
|
||||||
let plot_painter = ui.painter().with_clip_rect(rect);
|
|
||||||
|
|
||||||
plot_painter.add(epaint::RectShape {
|
|
||||||
rect,
|
|
||||||
rounding: Rounding::from(2.0),
|
|
||||||
fill: ui.visuals().extreme_bg_color,
|
|
||||||
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legend
|
|
||||||
let legend = legend_config
|
|
||||||
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
|
|
||||||
|
|
||||||
// Remove the deselected items.
|
|
||||||
items.retain(|item| !hidden_items.contains(item.name()));
|
|
||||||
lines.retain(|item| !hidden_items.contains(&item.name));
|
|
||||||
|
|
||||||
// Highlight the hovered items.
|
|
||||||
if let Some(hovered_name) = &hovered_entry {
|
|
||||||
items
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|entry| entry.name() == hovered_name)
|
|
||||||
.for_each(|entry| entry.highlight());
|
|
||||||
lines
|
|
||||||
.iter_mut()
|
|
||||||
.filter(|entry| &entry.name == hovered_name)
|
|
||||||
.for_each(|entry| {
|
|
||||||
entry.highlight();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Move highlighted items to front.
|
|
||||||
items.sort_by_key(|item| item.highlighted());
|
|
||||||
lines.sort_by_key(|item| item.highlighted());
|
|
||||||
|
|
||||||
// Set bounds automatically based on content.
|
|
||||||
if auto_bounds || !bounds.is_valid() {
|
|
||||||
bounds = min_auto_bounds;
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.for_each(|item| bounds.merge(&item.get_bounds()));
|
|
||||||
lines
|
|
||||||
.iter()
|
|
||||||
.for_each(|item| bounds.merge(&item.get_bounds()));
|
|
||||||
bounds.add_relative_margin(margin_fraction);
|
|
||||||
}
|
|
||||||
// Make sure they are not empty.
|
|
||||||
if !bounds.is_valid() {
|
|
||||||
bounds = Bounds::new_symmetrical(1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut transform = ScreenTransform::new(rect, bounds, false, false);
|
|
||||||
// Enforce equal aspect ratio.
|
|
||||||
if let Some(data_aspect) = data_aspect {
|
|
||||||
transform.set_aspect(data_aspect as f64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zooming
|
|
||||||
if allow_zoom {
|
|
||||||
if let Some(hover_pos) = response.hover_pos() {
|
|
||||||
let zoom_factor = if data_aspect.is_some() {
|
|
||||||
Vec2::splat(ui.input().zoom_delta())
|
|
||||||
} else {
|
|
||||||
ui.input().zoom_delta_2d()
|
|
||||||
};
|
|
||||||
if zoom_factor != Vec2::splat(1.0) {
|
|
||||||
transform.zoom(zoom_factor, hover_pos);
|
|
||||||
auto_bounds = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let scroll_delta = ui.input().scroll_delta;
|
|
||||||
if scroll_delta != Vec2::ZERO {
|
|
||||||
transform.translate_bounds(-scroll_delta);
|
|
||||||
auto_bounds = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize values from functions.
|
|
||||||
items
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|item| item.initialize(transform.bounds().range_x()));
|
|
||||||
lines
|
|
||||||
.iter_mut()
|
|
||||||
.for_each(|line| line.initialize(transform.bounds().range_x()));
|
|
||||||
|
|
||||||
let t_bounds = *transform.bounds();
|
|
||||||
|
|
||||||
let prepared = DragPlotPrepared {
|
|
||||||
items,
|
|
||||||
lines,
|
|
||||||
show_x: true,
|
|
||||||
show_y: true,
|
|
||||||
show_axes: [true, true],
|
|
||||||
transform: transform.clone(),
|
|
||||||
axis_names,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(mut f) = on_event {
|
|
||||||
if let Some(pointer) = response.hover_pos() {
|
|
||||||
if response.mouse_down(PointerButton::Primary) {
|
|
||||||
if let Some(idx) = prepared.find_clicked(pointer) {
|
|
||||||
f(PlotMsg::Clicked(idx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allow_drag
|
|
||||||
&& response.dragged_by(PointerButton::Primary)
|
|
||||||
&& response.hover_pos().is_some()
|
|
||||||
{
|
|
||||||
let mut delta = response.drag_delta();
|
|
||||||
delta.x *= transform.dvalue_dpos()[0] as f32;
|
|
||||||
delta.y *= transform.dvalue_dpos()[1] as f32;
|
|
||||||
f(PlotMsg::Drag(delta));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepared.ui(ui, &response);
|
|
||||||
|
|
||||||
if let Some(mut legend) = legend {
|
|
||||||
ui.add(&mut legend);
|
|
||||||
hidden_items = legend.get_hidden_items();
|
|
||||||
hovered_entry = legend.get_hovered_entry_name();
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.ctx()
|
|
||||||
.data()
|
|
||||||
.get_persisted_mut_or_insert_with(plot_id, || PlotMemory {
|
|
||||||
bounds: t_bounds,
|
|
||||||
auto_bounds,
|
|
||||||
hovered_entry,
|
|
||||||
hidden_items,
|
|
||||||
min_auto_bounds,
|
|
||||||
});
|
|
||||||
response.on_hover_cursor(CursorIcon::Crosshair)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PointerExt {
|
|
||||||
fn mouse_down(&self, pointer: PointerButton) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PointerExt for Response {
|
|
||||||
fn mouse_down(&self, p: PointerButton) -> bool {
|
|
||||||
let pointer = &self.ctx.input().pointer;
|
|
||||||
match p {
|
|
||||||
PointerButton::Primary => pointer.primary_down(),
|
|
||||||
PointerButton::Secondary => pointer.secondary_down(),
|
|
||||||
PointerButton::Middle => pointer.middle_down(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,277 +0,0 @@
|
|||||||
use egui::{emath, pos2, remap_clamp, vec2, Align2, Layout, NumExt, Pos2, Response, TextStyle, Ui};
|
|
||||||
use epaint::{Color32, Rgba, Shape, Stroke};
|
|
||||||
|
|
||||||
use crate::items::{Line, PlotItem, Value};
|
|
||||||
use crate::transform::ScreenTransform;
|
|
||||||
|
|
||||||
pub struct DragPlotPrepared {
|
|
||||||
pub items: Vec<Box<dyn PlotItem>>,
|
|
||||||
pub lines: Vec<Line>,
|
|
||||||
pub show_x: bool,
|
|
||||||
pub show_y: bool,
|
|
||||||
pub show_axes: [bool; 2],
|
|
||||||
pub transform: ScreenTransform,
|
|
||||||
pub axis_names: [String; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DragPlotPrepared {
|
|
||||||
pub fn ui(self, ui: &mut Ui, response: &Response) {
|
|
||||||
let mut shapes = Vec::new();
|
|
||||||
|
|
||||||
for d in 0..2 {
|
|
||||||
if self.show_axes[d] {
|
|
||||||
self.paint_axis(ui, d, &mut shapes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let transform = &self.transform;
|
|
||||||
|
|
||||||
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
|
|
||||||
plot_ui.set_clip_rect(*transform.frame());
|
|
||||||
|
|
||||||
for item in &self.items {
|
|
||||||
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
|
||||||
}
|
|
||||||
for item in &self.lines {
|
|
||||||
item.get_shapes(&mut plot_ui, transform, &mut shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pointer) = response.hover_pos() {
|
|
||||||
self.hover(ui, pointer, &mut shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.painter()
|
|
||||||
.with_clip_rect(*transform.frame())
|
|
||||||
.extend(shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
|
||||||
let Self { transform, .. } = self;
|
|
||||||
|
|
||||||
let bounds = transform.bounds();
|
|
||||||
let text_style = TextStyle::Body;
|
|
||||||
|
|
||||||
let base: i64 = 10;
|
|
||||||
let base_f = base as f64;
|
|
||||||
|
|
||||||
let min_line_spacing_in_points = 6.0;
|
|
||||||
let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points;
|
|
||||||
let step_size = base_f.powi(step_size.abs().log(base_f).ceil() as i32);
|
|
||||||
|
|
||||||
let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() as f32;
|
|
||||||
|
|
||||||
// Where on the cross-dimension to show the label values
|
|
||||||
let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]);
|
|
||||||
|
|
||||||
for i in 0.. {
|
|
||||||
let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor();
|
|
||||||
if value_main > bounds.max[axis] {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = if axis == 0 {
|
|
||||||
Value::new(value_main, value_cross)
|
|
||||||
} else {
|
|
||||||
Value::new(value_cross, value_main)
|
|
||||||
};
|
|
||||||
let pos_in_gui = transform.position_from_value(&value);
|
|
||||||
|
|
||||||
let n = (value_main / step_size).round() as i64;
|
|
||||||
let spacing_in_points = if n % (base * base) == 0 {
|
|
||||||
step_size_in_points * (base_f * base_f) as f32 // think line
|
|
||||||
// (multiple of
|
|
||||||
// 100)
|
|
||||||
} else if n % base == 0 {
|
|
||||||
step_size_in_points * base_f as f32 // medium line (multiple of
|
|
||||||
// 10)
|
|
||||||
} else {
|
|
||||||
step_size_in_points // thin line
|
|
||||||
};
|
|
||||||
|
|
||||||
let line_alpha = remap_clamp(
|
|
||||||
spacing_in_points,
|
|
||||||
(min_line_spacing_in_points as f32)..=300.0,
|
|
||||||
0.0..=0.15,
|
|
||||||
);
|
|
||||||
|
|
||||||
if line_alpha > 0.0 {
|
|
||||||
let line_color = color_from_alpha(ui, line_alpha);
|
|
||||||
|
|
||||||
let mut p0 = pos_in_gui;
|
|
||||||
let mut p1 = pos_in_gui;
|
|
||||||
p0[1 - axis] = transform.frame().min[1 - axis];
|
|
||||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
|
||||||
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4);
|
|
||||||
|
|
||||||
if text_alpha > 0.0 {
|
|
||||||
let color = color_from_alpha(ui, text_alpha);
|
|
||||||
let text = emath::round_to_decimals(value_main, 5).to_string(); // hack
|
|
||||||
|
|
||||||
let galley = ui.painter().layout_no_wrap(
|
|
||||||
text,
|
|
||||||
ui.style().text_styles.get(&text_style).cloned().unwrap(),
|
|
||||||
color,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y);
|
|
||||||
|
|
||||||
// Make sure we see the labels, even if the axis is off-screen:
|
|
||||||
text_pos[1 - axis] = text_pos[1 - axis]
|
|
||||||
.at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
|
|
||||||
.at_least(transform.frame().min[1 - axis] + 1.0);
|
|
||||||
|
|
||||||
shapes.push(Shape::galley(text_pos, galley));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 {
|
|
||||||
if ui.visuals().dark_mode {
|
|
||||||
Rgba::from_white_alpha(alpha).into()
|
|
||||||
} else {
|
|
||||||
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_clicked(&self, pointer: Pos2) -> Option<usize> {
|
|
||||||
let Self {
|
|
||||||
transform, lines, ..
|
|
||||||
} = self;
|
|
||||||
let interact_radius: f32 = 16.0;
|
|
||||||
let mut closest_value = None;
|
|
||||||
let mut closest_dist_sq = interact_radius.powi(2);
|
|
||||||
for values in lines.iter().filter_map(|v| v.values()) {
|
|
||||||
for (idx, value) in values.values.iter().enumerate() {
|
|
||||||
let pos = transform.position_from_value(value);
|
|
||||||
let dist_sq = pointer.distance_sq(pos);
|
|
||||||
if dist_sq < closest_dist_sq {
|
|
||||||
closest_dist_sq = dist_sq;
|
|
||||||
closest_value = Some(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closest_value
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) {
|
|
||||||
let Self {
|
|
||||||
transform,
|
|
||||||
show_x,
|
|
||||||
show_y,
|
|
||||||
items,
|
|
||||||
lines,
|
|
||||||
..
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
if !show_x && !show_y {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let interact_radius: f32 = 16.0;
|
|
||||||
let mut closest_value = None;
|
|
||||||
let mut closest_item = None;
|
|
||||||
let mut closest_dist_sq = interact_radius.powi(2);
|
|
||||||
for item in items {
|
|
||||||
if let Some(values) = item.values() {
|
|
||||||
for value in &values.values {
|
|
||||||
let pos = transform.position_from_value(value);
|
|
||||||
let dist_sq = pointer.distance_sq(pos);
|
|
||||||
if dist_sq < closest_dist_sq {
|
|
||||||
closest_dist_sq = dist_sq;
|
|
||||||
closest_value = Some(value);
|
|
||||||
closest_item = Some(item.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for item in lines {
|
|
||||||
if let Some(values) = item.values() {
|
|
||||||
for value in &values.values {
|
|
||||||
let pos = transform.position_from_value(value);
|
|
||||||
let dist_sq = pointer.distance_sq(pos);
|
|
||||||
if dist_sq < closest_dist_sq {
|
|
||||||
closest_dist_sq = dist_sq;
|
|
||||||
closest_value = Some(value);
|
|
||||||
closest_item = Some(&item.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prefix = String::new();
|
|
||||||
if let Some(name) = closest_item {
|
|
||||||
if !name.is_empty() {
|
|
||||||
prefix = format!("{}\n", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_color = if ui.visuals().dark_mode {
|
|
||||||
Color32::from_gray(100).additive()
|
|
||||||
} else {
|
|
||||||
Color32::from_black_alpha(180)
|
|
||||||
};
|
|
||||||
|
|
||||||
let value = if let Some(value) = closest_value {
|
|
||||||
let position = transform.position_from_value(value);
|
|
||||||
shapes.push(Shape::circle_filled(position, 3.0, line_color));
|
|
||||||
*value
|
|
||||||
} else {
|
|
||||||
transform.value_from_position(pointer)
|
|
||||||
};
|
|
||||||
let pointer = transform.position_from_value(&value);
|
|
||||||
|
|
||||||
let rect = transform.frame();
|
|
||||||
|
|
||||||
if *show_x {
|
|
||||||
// vertical line
|
|
||||||
shapes.push(Shape::line_segment(
|
|
||||||
[pos2(pointer.x, rect.top()), pos2(pointer.x, rect.bottom())],
|
|
||||||
(1.0, line_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if *show_y {
|
|
||||||
// horizontal line
|
|
||||||
shapes.push(Shape::line_segment(
|
|
||||||
[pos2(rect.left(), pointer.y), pos2(rect.right(), pointer.y)],
|
|
||||||
(1.0, line_color),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = {
|
|
||||||
let scale = transform.dvalue_dpos();
|
|
||||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
|
||||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
|
||||||
let [x_name, y_name] = &self.axis_names;
|
|
||||||
if *show_x && *show_y {
|
|
||||||
format!(
|
|
||||||
"{}{} = {:.*}\n{} = {:.*}",
|
|
||||||
prefix, x_name, x_decimals, value.x, y_name, y_decimals, value.y
|
|
||||||
)
|
|
||||||
} else if *show_x {
|
|
||||||
format!("{}{} = {:.*}", prefix, x_name, x_decimals, value.x)
|
|
||||||
} else if *show_y {
|
|
||||||
format!("{}{} = {:.*}", prefix, y_name, y_decimals, value.y)
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(clippy::explicit_auto_deref)]
|
|
||||||
shapes.push(Shape::text(
|
|
||||||
&*ui.fonts(),
|
|
||||||
pointer + vec2(3.0, -2.0),
|
|
||||||
Align2::LEFT_BOTTOM,
|
|
||||||
text,
|
|
||||||
ui.style()
|
|
||||||
.text_styles
|
|
||||||
.get(&TextStyle::Body)
|
|
||||||
.cloned()
|
|
||||||
.unwrap(),
|
|
||||||
ui.visuals().text_color(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
use std::string::String;
|
|
||||||
|
|
||||||
use egui::{
|
|
||||||
pos2, vec2, Align, PointerButton, Rect, Response, Sense, TextStyle, WidgetInfo, WidgetType,
|
|
||||||
};
|
|
||||||
use epaint::Color32;
|
|
||||||
|
|
||||||
/// Where to place the plot legend.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Corner {
|
|
||||||
LeftTop,
|
|
||||||
RightTop,
|
|
||||||
LeftBottom,
|
|
||||||
RightBottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Corner {
|
|
||||||
pub fn all() -> impl Iterator<Item = Corner> {
|
|
||||||
[
|
|
||||||
Corner::LeftTop,
|
|
||||||
Corner::RightTop,
|
|
||||||
Corner::LeftBottom,
|
|
||||||
Corner::RightBottom,
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The configuration for a plot legend.
|
|
||||||
#[derive(Clone, PartialEq)]
|
|
||||||
pub struct Legend {
|
|
||||||
pub text_style: TextStyle,
|
|
||||||
pub background_alpha: f32,
|
|
||||||
pub position: Corner,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Legend {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
text_style: TextStyle::Body,
|
|
||||||
background_alpha: 0.75,
|
|
||||||
position: Corner::RightTop,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Legend {
|
|
||||||
/// Which text style to use for the legend. Default: `TextStyle::Body`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn text_style(mut self, style: TextStyle) -> Self {
|
|
||||||
self.text_style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The alpha of the legend background. Default: `0.75`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn background_alpha(mut self, alpha: f32) -> Self {
|
|
||||||
self.background_alpha = alpha;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// In which corner to place the legend. Default: `Corner::RightTop`.
|
|
||||||
#[must_use]
|
|
||||||
pub fn position(mut self, corner: Corner) -> Self {
|
|
||||||
self.position = corner;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LegendEntry {
|
|
||||||
pub color: Color32,
|
|
||||||
pub checked: bool,
|
|
||||||
pub hovered: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LegendEntry {
|
|
||||||
pub fn new(color: Color32, checked: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
color,
|
|
||||||
checked,
|
|
||||||
hovered: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, text: String) -> Response {
|
|
||||||
let Self {
|
|
||||||
color,
|
|
||||||
checked,
|
|
||||||
hovered,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let galley = ui.fonts().layout_delayed_color(
|
|
||||||
text,
|
|
||||||
ui.style()
|
|
||||||
.text_styles
|
|
||||||
.get(&TextStyle::Body)
|
|
||||||
.unwrap()
|
|
||||||
.clone(),
|
|
||||||
f32::INFINITY,
|
|
||||||
);
|
|
||||||
|
|
||||||
let icon_size = galley.size().y;
|
|
||||||
let icon_spacing = icon_size / 5.0;
|
|
||||||
let total_extra = vec2(icon_size + icon_spacing, 0.0);
|
|
||||||
|
|
||||||
let desired_size = total_extra + galley.size();
|
|
||||||
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
|
|
||||||
|
|
||||||
response
|
|
||||||
.widget_info(|| WidgetInfo::selected(WidgetType::Checkbox, *checked, galley.text()));
|
|
||||||
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT;
|
|
||||||
|
|
||||||
let icon_position_x = if label_on_the_left {
|
|
||||||
rect.right() - icon_size / 2.0
|
|
||||||
} else {
|
|
||||||
rect.left() + icon_size / 2.0
|
|
||||||
};
|
|
||||||
let icon_position = pos2(icon_position_x, rect.center().y);
|
|
||||||
let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size));
|
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
|
|
||||||
painter.add(epaint::CircleShape {
|
|
||||||
center: icon_rect.center(),
|
|
||||||
radius: icon_size * 0.5,
|
|
||||||
fill: visuals.bg_fill,
|
|
||||||
stroke: visuals.bg_stroke,
|
|
||||||
});
|
|
||||||
|
|
||||||
if *checked {
|
|
||||||
let fill = if *color == Color32::TRANSPARENT {
|
|
||||||
ui.visuals().noninteractive().fg_stroke.color
|
|
||||||
} else {
|
|
||||||
*color
|
|
||||||
};
|
|
||||||
painter.add(epaint::Shape::circle_filled(
|
|
||||||
icon_rect.center(),
|
|
||||||
icon_size * 0.4,
|
|
||||||
fill,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_position_x = if label_on_the_left {
|
|
||||||
rect.right() - icon_size - icon_spacing - galley.size().x
|
|
||||||
} else {
|
|
||||||
rect.left() + icon_size + icon_spacing
|
|
||||||
};
|
|
||||||
|
|
||||||
let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y);
|
|
||||||
painter.galley_with_color(text_position, galley, visuals.text_color());
|
|
||||||
|
|
||||||
*checked ^= response.clicked_by(PointerButton::Primary);
|
|
||||||
*hovered = response.hovered();
|
|
||||||
|
|
||||||
response
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use egui::style::Margin;
|
|
||||||
use egui::{Align, Direction, Frame, Layout, Rect, Response, Ui, Widget};
|
|
||||||
use epaint::ahash::AHashSet;
|
|
||||||
use epaint::Color32;
|
|
||||||
|
|
||||||
use crate::items::PlotItem;
|
|
||||||
use crate::widgets::legend::{Corner, Legend, LegendEntry};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LegendWidget {
|
|
||||||
rect: Rect,
|
|
||||||
entries: BTreeMap<String, LegendEntry>,
|
|
||||||
config: Legend,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LegendWidget {
|
|
||||||
/// Create a new legend from items, the names of items that are hidden and
|
|
||||||
/// the style of the text. Returns `None` if the legend has no entries.
|
|
||||||
pub fn try_new(
|
|
||||||
rect: Rect,
|
|
||||||
config: Legend,
|
|
||||||
items: &[Box<dyn PlotItem>],
|
|
||||||
hidden_items: &AHashSet<String>,
|
|
||||||
) -> Option<Self> {
|
|
||||||
let mut entries: BTreeMap<String, LegendEntry> = BTreeMap::new();
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.filter(|item| !item.name().is_empty())
|
|
||||||
.for_each(|item| {
|
|
||||||
entries
|
|
||||||
.entry(item.name().to_string())
|
|
||||||
.and_modify(|entry| {
|
|
||||||
if entry.color != item.color() {
|
|
||||||
// Multiple items with different colors
|
|
||||||
entry.color = Color32::TRANSPARENT;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_insert_with(|| {
|
|
||||||
let color = item.color();
|
|
||||||
let checked = !hidden_items.contains(item.name());
|
|
||||||
LegendEntry::new(color, checked)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
(!entries.is_empty()).then_some(Self {
|
|
||||||
rect,
|
|
||||||
entries,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the names of the hidden items.
|
|
||||||
pub fn get_hidden_items(&self) -> AHashSet<String> {
|
|
||||||
self.entries
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, entry)| !entry.checked)
|
|
||||||
.map(|(name, _)| name.clone())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the name of the hovered items.
|
|
||||||
pub fn get_hovered_entry_name(&self) -> Option<String> {
|
|
||||||
self.entries
|
|
||||||
.iter()
|
|
||||||
.find(|(_, entry)| entry.hovered)
|
|
||||||
.map(|(name, _)| name.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Widget for &mut LegendWidget {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let LegendWidget {
|
|
||||||
rect,
|
|
||||||
entries,
|
|
||||||
config,
|
|
||||||
} = self;
|
|
||||||
|
|
||||||
let main_dir = match config.position {
|
|
||||||
Corner::LeftTop | Corner::RightTop => Direction::TopDown,
|
|
||||||
Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp,
|
|
||||||
};
|
|
||||||
let cross_align = match config.position {
|
|
||||||
Corner::LeftTop | Corner::LeftBottom => Align::LEFT,
|
|
||||||
Corner::RightTop | Corner::RightBottom => Align::RIGHT,
|
|
||||||
};
|
|
||||||
let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align);
|
|
||||||
let legend_pad = 4.0;
|
|
||||||
let legend_rect = rect.shrink(legend_pad);
|
|
||||||
let mut legend_ui = ui.child_ui(legend_rect, layout);
|
|
||||||
legend_ui
|
|
||||||
.scope(|ui| {
|
|
||||||
// ui.style_mut().body_text_style = config.text_style;
|
|
||||||
let background_frame = Frame {
|
|
||||||
inner_margin: Margin::symmetric(8.0, 4.0),
|
|
||||||
outer_margin: Default::default(),
|
|
||||||
rounding: ui.style().visuals.window_rounding,
|
|
||||||
shadow: epaint::Shadow::default(),
|
|
||||||
fill: ui.style().visuals.extreme_bg_color,
|
|
||||||
stroke: ui.style().visuals.window_stroke(),
|
|
||||||
}
|
|
||||||
.multiply_with_opacity(config.background_alpha);
|
|
||||||
background_frame
|
|
||||||
.show(ui, |ui| {
|
|
||||||
entries
|
|
||||||
.iter_mut()
|
|
||||||
.map(|(name, entry)| entry.ui(ui, name.clone()))
|
|
||||||
.reduce(|r1, r2| r1.union(r2))
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.inner
|
|
||||||
})
|
|
||||||
.inner
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
pub mod change_fan_settings;
|
|
||||||
pub mod config_file;
|
|
||||||
pub mod cooling_performance;
|
|
||||||
pub mod drag_plot;
|
|
||||||
pub mod drag_plot_prepared;
|
|
||||||
pub mod legend;
|
|
||||||
pub mod legend_widget;
|
|
||||||
pub mod output_widget;
|
|
||||||
pub mod outputs_settings;
|
|
||||||
pub mod reload_section;
|
|
||||||
|
|
||||||
pub use change_fan_settings::*;
|
|
||||||
pub use config_file::*;
|
|
||||||
pub use cooling_performance::*;
|
|
@ -1,58 +0,0 @@
|
|||||||
use amdgpu::pidfile::ports::Output;
|
|
||||||
use egui::{Response, Sense, Ui, Vec2};
|
|
||||||
use epaint::Stroke;
|
|
||||||
|
|
||||||
use crate::app::StatefulConfig;
|
|
||||||
|
|
||||||
pub struct OutputWidget<'output, 'stateful> {
|
|
||||||
output: &'output Output,
|
|
||||||
state: &'stateful mut StatefulConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'output, 'stateful> OutputWidget<'output, 'stateful> {
|
|
||||||
pub fn new(output: &'output Output, state: &'stateful mut StatefulConfig) -> Self {
|
|
||||||
Self { output, state }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'output, 'stateful> egui::Widget for OutputWidget<'output, 'stateful> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
let (rect, res) = ui.allocate_exact_size(Vec2::new(80.0, 80.0), Sense::click());
|
|
||||||
if let Some(handle) = self.output.ty.and_then(|ty| self.state.textures.get(&ty)) {
|
|
||||||
ui.image(handle.id(), handle.size_vec2());
|
|
||||||
} else {
|
|
||||||
let painter = ui.painter();
|
|
||||||
painter.rect_filled(rect, 0.0, epaint::color::Color32::DARK_RED);
|
|
||||||
painter.rect(rect, 2.0, epaint::color::Color32::DARK_RED, {
|
|
||||||
Stroke {
|
|
||||||
width: 1.0,
|
|
||||||
color: epaint::color::Color32::GREEN,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let rect_middle_point = (rect.max - rect.min) / 2.0;
|
|
||||||
|
|
||||||
painter.circle_filled(
|
|
||||||
rect.min + Vec2::new(rect_middle_point.x / 2.0, rect_middle_point.y),
|
|
||||||
3.0,
|
|
||||||
epaint::color::Color32::GREEN,
|
|
||||||
);
|
|
||||||
painter.circle_filled(
|
|
||||||
rect.min + rect_middle_point,
|
|
||||||
3.0,
|
|
||||||
epaint::color::Color32::GREEN,
|
|
||||||
);
|
|
||||||
|
|
||||||
painter.circle_filled(
|
|
||||||
rect.min
|
|
||||||
+ Vec2::new(
|
|
||||||
rect_middle_point.x + (rect_middle_point.x / 2.0),
|
|
||||||
rect_middle_point.y,
|
|
||||||
),
|
|
||||||
3.0,
|
|
||||||
epaint::color::Color32::GREEN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use amdgpu::pidfile::ports::{Output, Status};
|
|
||||||
use egui::{RichText, Ui, WidgetText};
|
|
||||||
use epaint::Color32;
|
|
||||||
|
|
||||||
use crate::app::StatefulConfig;
|
|
||||||
use crate::widgets::output_widget::OutputWidget;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct OutputsSettings {}
|
|
||||||
|
|
||||||
impl OutputsSettings {
|
|
||||||
pub fn draw(
|
|
||||||
&mut self,
|
|
||||||
ui: &mut Ui,
|
|
||||||
state: &mut StatefulConfig,
|
|
||||||
outputs: &BTreeMap<String, Vec<Output>>,
|
|
||||||
) {
|
|
||||||
let _available = ui.available_rect_before_wrap();
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
outputs.iter().for_each(|(name, outputs)| {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.label(format!("Card {name}"));
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
outputs.iter().for_each(|output| {
|
|
||||||
Self::render_single(ui, state, output);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_single(ui: &mut Ui, state: &mut StatefulConfig, output: &Output) {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.add(OutputWidget::new(output, state));
|
|
||||||
|
|
||||||
ui.label(format!("Port type {:?}", output.port_type));
|
|
||||||
ui.label(format!("Port number {}", output.port_number));
|
|
||||||
if let Some(name) = output.port_name.as_deref() {
|
|
||||||
ui.label(format!("Port name {}", name));
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.label(WidgetText::RichText(
|
|
||||||
RichText::new(match output.status {
|
|
||||||
Status::Connected => "Connected",
|
|
||||||
Status::Disconnected => "Disconnected",
|
|
||||||
})
|
|
||||||
.color(match output.status {
|
|
||||||
Status::Connected => Color32::GREEN,
|
|
||||||
Status::Disconnected => Color32::GRAY,
|
|
||||||
})
|
|
||||||
.code()
|
|
||||||
.strong()
|
|
||||||
.monospace(),
|
|
||||||
));
|
|
||||||
ui.label("Display Power Management");
|
|
||||||
ui.label(WidgetText::RichText(
|
|
||||||
RichText::new(match output.display_power_managment {
|
|
||||||
true => "On",
|
|
||||||
false => "Off",
|
|
||||||
})
|
|
||||||
.color(match output.display_power_managment {
|
|
||||||
true => Color32::GREEN,
|
|
||||||
false => Color32::GRAY,
|
|
||||||
})
|
|
||||||
.monospace()
|
|
||||||
.code()
|
|
||||||
.strong(),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
use amdgpu::pidfile::helper_cmd::Command;
|
|
||||||
use egui::{PointerButton, Response, Sense, Ui};
|
|
||||||
|
|
||||||
use crate::app::{ChangeState, FanServices};
|
|
||||||
|
|
||||||
pub struct ReloadSection<'l> {
|
|
||||||
pub services: &'l mut FanServices,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'l> egui::Widget for ReloadSection<'l> {
|
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.label("Reload config for service");
|
|
||||||
|
|
||||||
self.services.0.iter_mut().for_each(|service| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label(format!("PID {}", service.pid.0));
|
|
||||||
if ui.button("Reload").clicked_by(PointerButton::Primary) {
|
|
||||||
service.reload = ChangeState::Reloading;
|
|
||||||
|
|
||||||
match amdgpu::pidfile::helper_cmd::send_command(Command::ReloadConfig {
|
|
||||||
pid: service.pid,
|
|
||||||
}) {
|
|
||||||
Ok(response) => {
|
|
||||||
service.reload = ChangeState::Success;
|
|
||||||
tracing::info!("{:?}", response)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
service.reload = ChangeState::Failure(format!("{:?}", e));
|
|
||||||
tracing::error!("Failed to reload config. {:?}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match &service.reload {
|
|
||||||
ChangeState::New => {}
|
|
||||||
ChangeState::Reloading => {
|
|
||||||
ui.label("Reloading...");
|
|
||||||
}
|
|
||||||
ChangeState::Success => {
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new("Reloaded"), /* .text_color(Color32::DARK_GREEN) */
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ChangeState::Failure(msg) => {
|
|
||||||
ui.add(
|
|
||||||
egui::Label::new(format!("Failure. {}", msg)), // .text_color(Color32::RED),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.allocate_response(ui.available_size(), Sense::click())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'l> ReloadSection<'l> {
|
|
||||||
pub fn new(services: &'l mut FanServices) -> Self {
|
|
||||||
Self { services }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdmond-lib"
|
|
||||||
version = "1.0.10"
|
|
||||||
edition = "2021"
|
|
||||||
description = "AMD GPU monitoring tool for Linux"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["monitor", "fan"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0.126", features = ["derive"] }
|
|
||||||
toml = { version = "0.5.8" }
|
|
||||||
csv = { version = "1.1.6" }
|
|
||||||
|
|
||||||
thiserror = "1.0.30"
|
|
||||||
gumdrop = { version = "0.8.0" }
|
|
||||||
|
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
|
||||||
|
|
||||||
log = { version = "0.4.14" }
|
|
||||||
pretty_env_logger = { version = "0.4.0" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }
|
|
@ -1,26 +0,0 @@
|
|||||||
use amdgpu::utils;
|
|
||||||
use amdgpu_config::{fan, monitor};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AmdMonError {
|
|
||||||
#[error("Mon AMD GPU card was found")]
|
|
||||||
NoHwMon,
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("{0}")]
|
|
||||||
MonConfigError(#[from] monitor::ConfigError),
|
|
||||||
#[error("{0}")]
|
|
||||||
FanConfigError(#[from] fan::ConfigError),
|
|
||||||
#[error("{0}")]
|
|
||||||
AmdUtils(#[from] utils::AmdGpuError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Csv(#[from] csv::Error),
|
|
||||||
#[error("AMD GPU temperature is malformed. It should be number. {0:?}")]
|
|
||||||
NonIntTemp(std::num::ParseIntError),
|
|
||||||
#[error("AMD GPU fan speed is malformed. It should be number. {0:?}")]
|
|
||||||
NonIntPwm(std::num::ParseIntError),
|
|
||||||
#[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")]
|
|
||||||
InvalidMonitorFormat,
|
|
||||||
#[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")]
|
|
||||||
EmptyTempSet,
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
pub mod errors;
|
|
||||||
|
|
||||||
use amdgpu::hw_mon::HwMon;
|
|
||||||
use amdgpu::utils::load_temp_inputs;
|
|
||||||
use amdgpu::{
|
|
||||||
TempInput, PULSE_WIDTH_MODULATION, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN,
|
|
||||||
};
|
|
||||||
use amdgpu_config::fan;
|
|
||||||
|
|
||||||
use crate::errors::AmdMonError;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, AmdMonError>;
|
|
||||||
|
|
||||||
pub struct AmdMon {
|
|
||||||
temp_input: Option<TempInput>,
|
|
||||||
inputs: Vec<String>,
|
|
||||||
hw_mon: HwMon,
|
|
||||||
/// Minimal modulation (between 0-255)
|
|
||||||
pub pwm_min: Option<u32>,
|
|
||||||
/// Maximal modulation (between 0-255)
|
|
||||||
pub pwm_max: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for AmdMon {
|
|
||||||
type Target = HwMon;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.hw_mon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AmdMon {
|
|
||||||
pub fn wrap_all(mons: Vec<HwMon>, config: &fan::Config) -> Vec<Self> {
|
|
||||||
mons.into_iter()
|
|
||||||
.map(|hw_mon| Self::wrap(hw_mon, config))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self {
|
|
||||||
Self {
|
|
||||||
temp_input: config.temp_input().cloned(),
|
|
||||||
inputs: load_temp_inputs(&hw_mon),
|
|
||||||
hw_mon,
|
|
||||||
pwm_min: None,
|
|
||||||
pwm_max: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gpu_temp(&self) -> Vec<(String, crate::Result<f64>)> {
|
|
||||||
self.inputs
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|name| {
|
|
||||||
let temp = self
|
|
||||||
.read_gpu_temp(name.as_str())
|
|
||||||
.map(|temp| temp as f64 / 1000f64);
|
|
||||||
(name, temp)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gpu_temp_of(&self, input_idx: usize) -> Option<(&String, crate::Result<f64>)> {
|
|
||||||
self.inputs.get(input_idx).map(|name| {
|
|
||||||
let temp = self
|
|
||||||
.read_gpu_temp(name.as_str())
|
|
||||||
.map(|temp| temp as f64 / 1000f64);
|
|
||||||
(name, temp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_gpu_temp(&self, name: &str) -> crate::Result<u64> {
|
|
||||||
let value = self
|
|
||||||
.hw_mon_read(name)?
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(AmdMonError::NonIntTemp)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pwm(&self) -> crate::Result<u32> {
|
|
||||||
let value = self
|
|
||||||
.hw_mon_read(PULSE_WIDTH_MODULATION)?
|
|
||||||
.parse()
|
|
||||||
.map_err(AmdMonError::NonIntPwm)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pwm_min(&mut self) -> u32 {
|
|
||||||
if self.pwm_min.is_none() {
|
|
||||||
self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0));
|
|
||||||
};
|
|
||||||
self.pwm_min.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pwm_max(&mut self) -> u32 {
|
|
||||||
if self.pwm_max.is_none() {
|
|
||||||
self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255));
|
|
||||||
};
|
|
||||||
self.pwm_max.unwrap_or(255)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_gpu_temp(&self) -> crate::Result<f64> {
|
|
||||||
if let Some(input) = self.temp_input.as_ref() {
|
|
||||||
let value = self.read_gpu_temp(&input.as_string())?;
|
|
||||||
return Ok(value as f64 / 1000f64);
|
|
||||||
}
|
|
||||||
let mut results = Vec::with_capacity(self.inputs.len());
|
|
||||||
for name in self.inputs.iter() {
|
|
||||||
results.push(self.read_gpu_temp(name).unwrap_or(0));
|
|
||||||
}
|
|
||||||
results.sort_unstable();
|
|
||||||
let value = results
|
|
||||||
.last()
|
|
||||||
.copied()
|
|
||||||
.map(|temp| temp as f64 / 1000f64)
|
|
||||||
.ok_or(AmdMonError::EmptyTempSet)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdmond"
|
|
||||||
version = "1.0.10"
|
|
||||||
edition = "2021"
|
|
||||||
description = "AMD GPU monitoring tool for Linux"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["monitor", "fan"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0.10" }
|
|
||||||
|
|
||||||
serde = { version = "1.0.126", features = ["derive"] }
|
|
||||||
toml = { version = "0.5.8" }
|
|
||||||
csv = { version = "1.1.6" }
|
|
||||||
|
|
||||||
thiserror = { version = "1.0.30" }
|
|
||||||
gumdrop = { version = "0.8.0" }
|
|
||||||
|
|
||||||
chrono = { version = "0.4.19", features = ["serde"] }
|
|
||||||
|
|
||||||
log = { version = "0.4.14" }
|
|
||||||
pretty_env_logger = { version = "0.4.0" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] }
|
|
||||||
amdmond-lib = { path = "../amdmond-lib", version = "1.0" }
|
|
@ -1,66 +0,0 @@
|
|||||||
# AMD Monitoring daemon
|
|
||||||
|
|
||||||
## Watch mode
|
|
||||||
|
|
||||||
Tool will check temperature and prints:
|
|
||||||
|
|
||||||
* Minimal modulation
|
|
||||||
* Maximal modulation
|
|
||||||
* Current modulation
|
|
||||||
* Current fan speed in percentage (PWD / PWD MAX * 100)
|
|
||||||
* Current value of each temperature sensor (typically temp1_input is which should be observed)
|
|
||||||
|
|
||||||
> `modulation` is a value between 0-255 which indicate how fast fan should be moving
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/usr/bin/amdmond watch --format short
|
|
||||||
```
|
|
||||||
|
|
||||||
### Formats
|
|
||||||
|
|
||||||
There are 2 possible formats.
|
|
||||||
|
|
||||||
* `short` - very compact info
|
|
||||||
* `long` - more human-readable info
|
|
||||||
|
|
||||||
## Log File mode
|
|
||||||
|
|
||||||
This tool can be used to track GPU temperature and amdfand speed curve management to prevent GPU card from generating
|
|
||||||
unnecessary noise.
|
|
||||||
|
|
||||||
It will create csv log file with:
|
|
||||||
|
|
||||||
* time
|
|
||||||
* temperature
|
|
||||||
* card modulation
|
|
||||||
* matrix point temperature
|
|
||||||
* matrix point speed
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/usr/bin/amdmond log_file -s /var/log/amdmon.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install amdmond
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### minimal:
|
|
||||||
|
|
||||||
```
|
|
||||||
amdmond log_file -s /var/log/amdmon.csv
|
|
||||||
```
|
|
||||||
|
|
||||||
Required arguments:
|
|
||||||
|
|
||||||
* `-s`, `--stat-file STAT-FILE` Full path to statistics file
|
|
||||||
|
|
||||||
Optional arguments:
|
|
||||||
|
|
||||||
* `-h`, `--help` Help message
|
|
||||||
* `-v`, `--version` Print version
|
|
||||||
* `-c`, `--config CONFIG` Config location
|
|
||||||
* `-i`, `--interval INTERVAL` Time between each check. 1000 is 1s, by default 5s
|
|
@ -1,13 +0,0 @@
|
|||||||
use crate::{log_file, watch};
|
|
||||||
|
|
||||||
#[derive(gumdrop::Options)]
|
|
||||||
pub enum Command {
|
|
||||||
Watch(watch::Watch),
|
|
||||||
LogFile(log_file::LogFile),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Command {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Watch(watch::Watch::default())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
use amdgpu::utils::hw_mons;
|
|
||||||
use amdgpu_config::fan;
|
|
||||||
use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH;
|
|
||||||
use amdgpu_config::monitor::Config;
|
|
||||||
use amdmond_lib::errors::AmdMonError;
|
|
||||||
use amdmond_lib::AmdMon;
|
|
||||||
|
|
||||||
#[derive(gumdrop::Options)]
|
|
||||||
pub struct LogFile {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
#[options(help = "Full path to statistics file")]
|
|
||||||
stat_file: String,
|
|
||||||
#[options(help = "Time between each check. 1000 is 1s, by default 5s")]
|
|
||||||
interval: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
|
||||||
struct Stat {
|
|
||||||
time: chrono::DateTime<chrono::Local>,
|
|
||||||
temperature: f64,
|
|
||||||
modulation: u32,
|
|
||||||
speed_setting: f64,
|
|
||||||
temperature_setting: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(command: LogFile, config: Config) -> amdmond_lib::Result<()> {
|
|
||||||
let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?;
|
|
||||||
|
|
||||||
let duration = std::time::Duration::new(
|
|
||||||
0,
|
|
||||||
command.interval.unwrap_or_else(|| config.interval()) * 1_000_000,
|
|
||||||
);
|
|
||||||
log::info!("Updating each: {:?}", duration);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_file(command.stat_file.as_str());
|
|
||||||
let stat_file = std::fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(false)
|
|
||||||
.write(true)
|
|
||||||
.open(command.stat_file.as_str())?;
|
|
||||||
|
|
||||||
let mut writer = csv::WriterBuilder::new()
|
|
||||||
.double_quote(true)
|
|
||||||
.has_headers(true)
|
|
||||||
.buffer_capacity(100)
|
|
||||||
.from_writer(stat_file);
|
|
||||||
|
|
||||||
let mon = {
|
|
||||||
let mons = hw_mons(true)?;
|
|
||||||
if mons.is_empty() {
|
|
||||||
return Err(AmdMonError::NoHwMon);
|
|
||||||
}
|
|
||||||
AmdMon::wrap(hw_mons(true)?.remove(0), &fan_config)
|
|
||||||
};
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let time = chrono::Local::now();
|
|
||||||
let temperature = {
|
|
||||||
let mut temperatures = mon.gpu_temp();
|
|
||||||
|
|
||||||
if let Some(input) = fan_config.temp_input() {
|
|
||||||
let input_name = input.as_string();
|
|
||||||
temperatures
|
|
||||||
.into_iter()
|
|
||||||
.find(|(name, _value)| name == &input_name)
|
|
||||||
.and_then(|(_, v)| v.ok())
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else if temperatures.len() > 1 {
|
|
||||||
temperatures.remove(1).1.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
temperatures
|
|
||||||
.get(0)
|
|
||||||
.and_then(|(_, v)| v.as_ref().ok().copied())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let (speed_setting, temperature_setting) =
|
|
||||||
if let Some(speed_matrix_point) = fan_config.speed_matrix_point(temperature) {
|
|
||||||
(speed_matrix_point.speed, speed_matrix_point.temp)
|
|
||||||
} else {
|
|
||||||
Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let stat = Stat {
|
|
||||||
time,
|
|
||||||
temperature,
|
|
||||||
modulation: mon.pwm()?,
|
|
||||||
speed_setting,
|
|
||||||
temperature_setting,
|
|
||||||
};
|
|
||||||
|
|
||||||
writer.serialize(stat)?;
|
|
||||||
writer.flush()?;
|
|
||||||
|
|
||||||
std::thread::sleep(duration);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
mod command;
|
|
||||||
mod log_file;
|
|
||||||
mod watch;
|
|
||||||
|
|
||||||
use amdgpu::utils::ensure_config_dir;
|
|
||||||
use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH};
|
|
||||||
use gumdrop::Options;
|
|
||||||
|
|
||||||
use crate::command::Command;
|
|
||||||
|
|
||||||
#[derive(gumdrop::Options)]
|
|
||||||
pub struct Opts {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
pub help: bool,
|
|
||||||
#[options(help = "Print version")]
|
|
||||||
pub version: bool,
|
|
||||||
#[options(help = "Config location")]
|
|
||||||
pub config: Option<String>,
|
|
||||||
#[options(command)]
|
|
||||||
pub command: Option<Command>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(config: Config) -> amdmond_lib::Result<()> {
|
|
||||||
let opts: Opts = Opts::parse_args_default_or_exit();
|
|
||||||
|
|
||||||
if opts.version {
|
|
||||||
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
match opts.command {
|
|
||||||
Some(Command::Watch(w)) => watch::run(w, config),
|
|
||||||
Some(Command::LogFile(l)) => log_file::run(l, config),
|
|
||||||
_ => {
|
|
||||||
println!("{}", <Opts as gumdrop::Options>::usage());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup() -> amdmond_lib::Result<(String, Config)> {
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "DEBUG");
|
|
||||||
}
|
|
||||||
pretty_env_logger::init();
|
|
||||||
ensure_config_dir()?;
|
|
||||||
|
|
||||||
let config_path = Opts::parse_args_default_or_exit()
|
|
||||||
.config
|
|
||||||
.unwrap_or_else(|| DEFAULT_MONITOR_CONFIG_PATH.to_string());
|
|
||||||
let config = load_config(&config_path)?;
|
|
||||||
log::info!("{:?}", config);
|
|
||||||
log::set_max_level(config.log_level().as_str().parse().unwrap());
|
|
||||||
Ok((config_path, config))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> amdmond_lib::Result<()> {
|
|
||||||
let (_config_path, config) = match setup() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match run(config) {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdportsd"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", features = ["gui-helper"] }
|
|
||||||
|
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
|
||||||
futures = { version = "0.3", features = [] }
|
|
||||||
ron = { version = "0.7.1" }
|
|
||||||
serde = { version = "1.0.137", features = ["derive"] }
|
|
||||||
|
|
||||||
tracing = { version = "0.1.36" }
|
|
||||||
tracing-subscriber = { version = "0.3.15" }
|
|
@ -1,156 +0,0 @@
|
|||||||
use std::fs::{DirEntry, Permissions};
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use amdgpu::pidfile::ports::{sock_file, Command, Response, *};
|
|
||||||
use amdgpu::IoFailure;
|
|
||||||
|
|
||||||
fn parse_output(entry: DirEntry) -> Option<Output> {
|
|
||||||
let ty = entry.file_type().ok()?;
|
|
||||||
if ty.is_dir() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
tracing::error!("{:?}", entry.path());
|
|
||||||
let file_name = entry.file_name();
|
|
||||||
let path = file_name.to_str()?;
|
|
||||||
let mut it = path
|
|
||||||
.split('-')
|
|
||||||
.map(String::from)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.into_iter();
|
|
||||||
|
|
||||||
let modes = std::fs::read_to_string(entry.path().join("modes"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
.lines()
|
|
||||||
.filter_map(|s| {
|
|
||||||
let mut it = s.split('x');
|
|
||||||
let width = it.next().and_then(|s| s.parse::<u16>().ok())?;
|
|
||||||
let height = it.next().and_then(|s| s.parse::<u16>().ok())?;
|
|
||||||
Some(OutputMode { width, height })
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let card = it.next()?.strip_prefix("card")?.to_string();
|
|
||||||
let port_type = it.next()?;
|
|
||||||
let dpms = std::fs::read_to_string(entry.path().join("dpms"))
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
tracing::error!("{}", e);
|
|
||||||
"Off".into()
|
|
||||||
})
|
|
||||||
.to_lowercase();
|
|
||||||
tracing::info!("Display Power Management System is {:?}", dpms);
|
|
||||||
|
|
||||||
let mut output = Output {
|
|
||||||
card,
|
|
||||||
ty: OutputType::parse_str(&port_type),
|
|
||||||
port_type,
|
|
||||||
modes,
|
|
||||||
display_power_managment: dpms.trim() == "on",
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut it = it.rev();
|
|
||||||
output.port_number = it.next()?.parse().ok()?;
|
|
||||||
|
|
||||||
let mut it = it.rev().peekable();
|
|
||||||
|
|
||||||
if it.peek().is_some() {
|
|
||||||
output.port_name = Some(it.collect::<Vec<_>>().join("-"));
|
|
||||||
}
|
|
||||||
|
|
||||||
output.status = output.read_status()?;
|
|
||||||
|
|
||||||
Some(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_outputs(state: Arc<Mutex<Vec<Output>>>) {
|
|
||||||
loop {
|
|
||||||
let outputs = std::fs::read_dir("/sys/class/drm")
|
|
||||||
.unwrap()
|
|
||||||
.filter_map(|r| r.ok())
|
|
||||||
.filter(|e| {
|
|
||||||
e.path()
|
|
||||||
.to_str()
|
|
||||||
.map(|s| s.contains("card"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
})
|
|
||||||
.filter_map(parse_output)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if let Ok(mut lock) = state.lock() {
|
|
||||||
*lock = outputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_millis(1_000 / 3)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Service = amdgpu::pidfile::Service<Response>;
|
|
||||||
|
|
||||||
async fn service(state: Arc<Mutex<Vec<Output>>>) {
|
|
||||||
let sock_path = sock_file();
|
|
||||||
let listener = {
|
|
||||||
let _ = std::fs::remove_file(&sock_path);
|
|
||||||
|
|
||||||
UnixListener::bind(&sock_path)
|
|
||||||
.map_err(|io| IoFailure {
|
|
||||||
io,
|
|
||||||
path: sock_path.clone(),
|
|
||||||
})
|
|
||||||
.expect("Creating pid file for ports failed")
|
|
||||||
};
|
|
||||||
if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) {
|
|
||||||
tracing::error!("Failed to change gui helper socket file mode. {:?}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Ok((stream, _addr)) = listener.accept() {
|
|
||||||
handle_connection(stream, state.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_connection(stream: UnixStream, state: Arc<Mutex<Vec<Output>>>) {
|
|
||||||
let mut service = Service::new(stream);
|
|
||||||
|
|
||||||
let command = match service.read_command() {
|
|
||||||
Some(s) => s,
|
|
||||||
_ => return service.kill(),
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!("Incoming {:?}", command);
|
|
||||||
let cmd = match ron::from_str::<Command>(command.trim()) {
|
|
||||||
Ok(cmd) => cmd,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Invalid message {:?}. {:?}", command, e);
|
|
||||||
return service.kill();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handle_command(service, cmd, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_command(mut service: Service, cmd: Command, state: Arc<Mutex<Vec<Output>>>) {
|
|
||||||
match cmd {
|
|
||||||
Command::Ports => {
|
|
||||||
if let Ok(outputs) = state.lock() {
|
|
||||||
service.write_response(Response::Ports(outputs.iter().map(Clone::clone).collect()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
let executor = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let state = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
|
|
||||||
executor.block_on(async {
|
|
||||||
let sync = read_outputs(state.clone());
|
|
||||||
let handle = service(state);
|
|
||||||
|
|
||||||
tokio::join!(sync, handle);
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-unknown-linux-musl"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
@ -1,25 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "amdvold"
|
|
||||||
version = "1.0.10"
|
|
||||||
edition = "2018"
|
|
||||||
description = "AMDGPU fan control service"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
keywords = ["hardware", "amdgpu"]
|
|
||||||
categories = ["hardware-support"]
|
|
||||||
repository = "https://github.com/Eraden/amdgpud"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0.11" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["voltage"] }
|
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
toml = { version = "0.5" }
|
|
||||||
thiserror = { version = "1.0" }
|
|
||||||
gumdrop = { version = "0.8" }
|
|
||||||
|
|
||||||
log = { version = "0.4" }
|
|
||||||
pretty_env_logger = { version = "0.4" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
amdgpu = { path = "../amdgpu", version = "1.0" }
|
|
||||||
amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["voltage"] }
|
|
@ -1,51 +0,0 @@
|
|||||||
# AMD graphic card voltage manager
|
|
||||||
|
|
||||||
This tool can be used to overclock you AMD graphic card on Linux
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install amdvold
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Available commands:
|
|
||||||
|
|
||||||
* `setup-info` - prints information how to enable voltage management on Linux (see Requirements)
|
|
||||||
* `print-states` - prints current card states
|
|
||||||
* `change-state` - change card voltage states
|
|
||||||
* `apply-changes` - apply changes
|
|
||||||
|
|
||||||
## Changing states
|
|
||||||
|
|
||||||
Positional arguments:
|
|
||||||
* `index` Profile number
|
|
||||||
* `module` Either memory or engine
|
|
||||||
* `frequency` New GPU module frequency
|
|
||||||
* `voltage` New GPU module voltage
|
|
||||||
|
|
||||||
Optional arguments:
|
|
||||||
* `-a`, `--apply-immediately` Apply changes immediately after change
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
amdvold 1 engine 1450MHz 772mV
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following:
|
|
||||||
|
|
||||||
* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example:
|
|
||||||
|
|
||||||
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1"
|
|
||||||
|
|
||||||
Easiest way is to modify "/etc/default/grub" and generate new grub config.
|
|
||||||
|
|
||||||
* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example:
|
|
||||||
|
|
||||||
options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff
|
|
||||||
|
|
||||||
(only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff")
|
|
@ -1,13 +0,0 @@
|
|||||||
To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following:
|
|
||||||
|
|
||||||
* In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example:
|
|
||||||
|
|
||||||
GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1"
|
|
||||||
|
|
||||||
Easiest way is to modify "/etc/default/grub" and generate new grub config.
|
|
||||||
|
|
||||||
* If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example:
|
|
||||||
|
|
||||||
options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff
|
|
||||||
|
|
||||||
(only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff")
|
|
@ -1,19 +0,0 @@
|
|||||||
use amdgpu::utils::hw_mons;
|
|
||||||
|
|
||||||
use crate::command::VoltageManipulator;
|
|
||||||
use crate::{Config, VoltageError};
|
|
||||||
|
|
||||||
#[derive(Debug, gumdrop::Options)]
|
|
||||||
pub struct ApplyChanges {
|
|
||||||
help: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(_command: ApplyChanges, config: &Config) -> crate::Result<()> {
|
|
||||||
let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config);
|
|
||||||
if mons.is_empty() {
|
|
||||||
return Err(VoltageError::NoAmdGpu);
|
|
||||||
}
|
|
||||||
let mon = mons.remove(0);
|
|
||||||
mon.write_apply()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
use amdgpu::utils::hw_mons;
|
|
||||||
|
|
||||||
use crate::clock_state::{Frequency, Voltage};
|
|
||||||
use crate::command::{HardwareModule, VoltageManipulator};
|
|
||||||
use crate::{Config, VoltageError};
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum ChangeStateError {
|
|
||||||
#[error("No profile index was given")]
|
|
||||||
Index,
|
|
||||||
#[error("No frequency was given")]
|
|
||||||
Freq,
|
|
||||||
#[error("No voltage was given")]
|
|
||||||
Voltage,
|
|
||||||
#[error("No AMD GPU module was given (either memory or engine)")]
|
|
||||||
Module,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, gumdrop::Options)]
|
|
||||||
pub struct ChangeState {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
#[options(help = "Profile number", free)]
|
|
||||||
index: u16,
|
|
||||||
#[options(help = "Either memory or engine", free)]
|
|
||||||
module: Option<HardwareModule>,
|
|
||||||
#[options(help = "New GPU module frequency", free)]
|
|
||||||
frequency: Option<Frequency>,
|
|
||||||
#[options(help = "New GPU module voltage", free)]
|
|
||||||
voltage: Option<Voltage>,
|
|
||||||
#[options(help = "Apply changes immediately after change")]
|
|
||||||
apply_immediately: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(command: ChangeState, config: &Config) -> crate::Result<()> {
|
|
||||||
let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config);
|
|
||||||
if mons.is_empty() {
|
|
||||||
return Err(VoltageError::NoAmdGpu);
|
|
||||||
}
|
|
||||||
let mon = mons.remove(0);
|
|
||||||
let ChangeState {
|
|
||||||
help: _,
|
|
||||||
index,
|
|
||||||
module,
|
|
||||||
frequency,
|
|
||||||
voltage,
|
|
||||||
apply_immediately,
|
|
||||||
} = command;
|
|
||||||
mon.write_state(
|
|
||||||
index,
|
|
||||||
frequency.ok_or(ChangeStateError::Freq)?,
|
|
||||||
voltage.ok_or(ChangeStateError::Voltage)?,
|
|
||||||
module.ok_or(ChangeStateError::Module)?,
|
|
||||||
)?;
|
|
||||||
if apply_immediately {
|
|
||||||
mon.write_apply()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,432 +0,0 @@
|
|||||||
use std::iter::Peekable;
|
|
||||||
use std::str::Chars;
|
|
||||||
|
|
||||||
const ENGINE_CLOCK_LABEL: &str = "OD_SCLK:";
|
|
||||||
const MEMORY_CLOCK_LABEL: &str = "OD_MCLK:";
|
|
||||||
const CURVE_POINTS_LABEL: &str = "OD_VDDC_CURVE:";
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct Frequency {
|
|
||||||
pub value: u32,
|
|
||||||
pub unit: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for Frequency {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
format!("{}{}", self.value, self.unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Frequency {
|
|
||||||
type Err = ClockStateError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut buffer = String::with_capacity(8);
|
|
||||||
let mut value = None;
|
|
||||||
for c in s.trim().chars() {
|
|
||||||
if c.is_numeric() && value.is_none() {
|
|
||||||
buffer.push(c);
|
|
||||||
} else if c.is_numeric() {
|
|
||||||
return Err(ClockStateError::NotFrequency(s.to_string()));
|
|
||||||
} else if value.is_none() {
|
|
||||||
if buffer.is_empty() {
|
|
||||||
return Err(ClockStateError::NotFrequency(s.to_string()));
|
|
||||||
}
|
|
||||||
value = Some(buffer.parse()?);
|
|
||||||
buffer.clear();
|
|
||||||
buffer.push(c);
|
|
||||||
} else {
|
|
||||||
buffer.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let value = value.ok_or_else(|| ClockStateError::NotFrequency(s.to_string()))?;
|
|
||||||
if !buffer.ends_with("hz") && !buffer.ends_with("Hz") {
|
|
||||||
return Err(ClockStateError::NotFrequency(s.to_string()));
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
value,
|
|
||||||
unit: buffer,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct Voltage {
|
|
||||||
pub value: u32,
|
|
||||||
pub unit: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for Voltage {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
format!("{}{}", self.value, self.unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for Voltage {
|
|
||||||
type Err = ClockStateError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut buffer = String::with_capacity(8);
|
|
||||||
let mut value = None;
|
|
||||||
for c in s.trim().chars() {
|
|
||||||
if c.is_numeric() && value.is_none() {
|
|
||||||
buffer.push(c);
|
|
||||||
} else if c.is_numeric() {
|
|
||||||
return Err(ClockStateError::NotVoltage(s.to_string()));
|
|
||||||
} else if value.is_none() {
|
|
||||||
if buffer.is_empty() {
|
|
||||||
return Err(ClockStateError::NotVoltage(s.to_string()));
|
|
||||||
}
|
|
||||||
value = Some(buffer.parse()?);
|
|
||||||
buffer.clear();
|
|
||||||
buffer.push(c);
|
|
||||||
} else {
|
|
||||||
buffer.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let value = value.ok_or_else(|| ClockStateError::NotVoltage(s.to_string()))?;
|
|
||||||
if !buffer.ends_with('V') {
|
|
||||||
return Err(ClockStateError::NotVoltage(s.to_string()));
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
value,
|
|
||||||
unit: buffer,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct CurvePoint {
|
|
||||||
pub freq: Frequency,
|
|
||||||
pub voltage: Voltage,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
|
||||||
pub enum ClockStateError {
|
|
||||||
#[error("Can't parse value. {0:?}")]
|
|
||||||
ParseValue(#[from] std::num::ParseIntError),
|
|
||||||
#[error("Value {0:?} is not a voltage")]
|
|
||||||
NotVoltage(String),
|
|
||||||
#[error("Value {0:?} is not a frequency")]
|
|
||||||
NotFrequency(String),
|
|
||||||
#[error("Voltage section for engine clock is not valid. Line {0:?} is malformed")]
|
|
||||||
InvalidEngineClockSection(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct ClockState {
|
|
||||||
pub curve_labels: Vec<CurvePoint>,
|
|
||||||
pub engine_label_lowest: Option<Frequency>,
|
|
||||||
pub engine_label_highest: Option<Frequency>,
|
|
||||||
pub memory_label_lowest: Option<Frequency>,
|
|
||||||
pub memory_label_highest: Option<Frequency>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ClockState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
curve_labels: Vec::with_capacity(3),
|
|
||||||
engine_label_lowest: None,
|
|
||||||
engine_label_highest: None,
|
|
||||||
memory_label_lowest: None,
|
|
||||||
memory_label_highest: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for ClockState {
|
|
||||||
type Err = ClockStateError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut clock_state = Self::default();
|
|
||||||
enum State {
|
|
||||||
Unknown,
|
|
||||||
ParseEngineClock,
|
|
||||||
ParseMemoryClock,
|
|
||||||
ParseCurve,
|
|
||||||
}
|
|
||||||
let mut state = State::Unknown;
|
|
||||||
for line in s.lines() {
|
|
||||||
let start = match line.chars().position(|c| c != ' ' && c != '\0') {
|
|
||||||
Some(idx) => idx,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let line = line[start..].trim();
|
|
||||||
match state {
|
|
||||||
_ if line == "OD_RANGE:" => break,
|
|
||||||
_ if line == ENGINE_CLOCK_LABEL => {
|
|
||||||
state = State::ParseEngineClock;
|
|
||||||
}
|
|
||||||
_ if line == MEMORY_CLOCK_LABEL => {
|
|
||||||
state = State::ParseMemoryClock;
|
|
||||||
}
|
|
||||||
_ if line == CURVE_POINTS_LABEL => {
|
|
||||||
state = State::ParseCurve;
|
|
||||||
}
|
|
||||||
State::ParseEngineClock => {
|
|
||||||
if clock_state.engine_label_lowest.is_none() {
|
|
||||||
clock_state.engine_label_lowest = Some(parse_freq_line(line)?);
|
|
||||||
} else {
|
|
||||||
clock_state.engine_label_highest = Some(parse_freq_line(line)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::ParseMemoryClock => {
|
|
||||||
if clock_state.memory_label_lowest.is_none() {
|
|
||||||
clock_state.memory_label_lowest = Some(parse_freq_line(line)?);
|
|
||||||
} else {
|
|
||||||
clock_state.memory_label_highest = Some(parse_freq_line(line)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::ParseCurve => {
|
|
||||||
let (freq, volt) = parse_freq_voltage_line(line)?;
|
|
||||||
clock_state.curve_labels.push(CurvePoint {
|
|
||||||
freq,
|
|
||||||
voltage: volt,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(clock_state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_mode_number<'line>(
|
|
||||||
line: &'line str,
|
|
||||||
chars: &mut Peekable<Chars<'line>>,
|
|
||||||
) -> std::result::Result<(), ClockStateError> {
|
|
||||||
let mut buffer = String::with_capacity(4);
|
|
||||||
while chars.peek().filter(|c| c.is_numeric()).is_some() {
|
|
||||||
buffer.push(chars.next().unwrap());
|
|
||||||
}
|
|
||||||
if buffer.is_empty() {
|
|
||||||
return Err(ClockStateError::InvalidEngineClockSection(line.to_string()));
|
|
||||||
}
|
|
||||||
chars
|
|
||||||
.next()
|
|
||||||
.filter(|c| *c == ':')
|
|
||||||
.ok_or_else(|| ClockStateError::InvalidEngineClockSection(line.to_string()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_freq(chars: &mut Peekable<Chars>) -> std::result::Result<Frequency, ClockStateError> {
|
|
||||||
consume_white(chars);
|
|
||||||
chars
|
|
||||||
.take_while(|c| *c != ' ')
|
|
||||||
.collect::<String>()
|
|
||||||
.parse::<Frequency>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_voltage(chars: &mut Peekable<Chars>) -> std::result::Result<Voltage, ClockStateError> {
|
|
||||||
consume_white(chars);
|
|
||||||
chars
|
|
||||||
.take_while(|c| *c != ' ')
|
|
||||||
.collect::<String>()
|
|
||||||
.parse::<Voltage>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_white(chars: &mut Peekable<Chars>) {
|
|
||||||
while chars.peek().filter(|c| **c == ' ').is_some() {
|
|
||||||
let _ = chars.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_freq_line(line: &str) -> std::result::Result<Frequency, ClockStateError> {
|
|
||||||
let mut chars = line.chars().peekable();
|
|
||||||
consume_mode_number(line, &mut chars)?;
|
|
||||||
consume_freq(&mut chars)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_freq_voltage_line(
|
|
||||||
line: &str,
|
|
||||||
) -> std::result::Result<(Frequency, Voltage), ClockStateError> {
|
|
||||||
let mut chars = line.chars().peekable();
|
|
||||||
consume_mode_number(line, &mut chars)?;
|
|
||||||
let freq = consume_freq(&mut chars)?;
|
|
||||||
consume_white(&mut chars);
|
|
||||||
Ok((freq, consume_voltage(&mut chars)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod parse_frequency {
|
|
||||||
use crate::clock_state::{ClockStateError, Frequency};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_empty_string() {
|
|
||||||
assert_eq!(
|
|
||||||
"".parse::<Frequency>(),
|
|
||||||
Err(ClockStateError::NotFrequency("".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_only_v_letter() {
|
|
||||||
assert_eq!(
|
|
||||||
"v".parse::<Frequency>(),
|
|
||||||
Err(ClockStateError::NotFrequency("v".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_only_hz() {
|
|
||||||
assert_eq!(
|
|
||||||
"hz".parse::<Frequency>(),
|
|
||||||
Err(ClockStateError::NotFrequency("hz".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_only_mhz() {
|
|
||||||
assert_eq!(
|
|
||||||
"Mhz".parse::<Frequency>(),
|
|
||||||
Err(ClockStateError::NotFrequency("Mhz".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_0mhz() {
|
|
||||||
assert_eq!(
|
|
||||||
"0Mhz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 0,
|
|
||||||
unit: "Mhz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_0khz() {
|
|
||||||
assert_eq!(
|
|
||||||
"0khz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 0,
|
|
||||||
unit: "khz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_0kz() {
|
|
||||||
assert_eq!(
|
|
||||||
"0hz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 0,
|
|
||||||
unit: "hz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_123mhz() {
|
|
||||||
assert_eq!(
|
|
||||||
"123Mhz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 123,
|
|
||||||
unit: "Mhz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_123khz() {
|
|
||||||
assert_eq!(
|
|
||||||
"123khz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 123,
|
|
||||||
unit: "khz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_123kz() {
|
|
||||||
assert_eq!(
|
|
||||||
"123hz".parse::<Frequency>(),
|
|
||||||
Ok(Frequency {
|
|
||||||
value: 123,
|
|
||||||
unit: "hz".to_string(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod state_tests {
|
|
||||||
use crate::clock_state::{ClockState, CurvePoint, Frequency, Voltage};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn valid_string() {
|
|
||||||
let s = r#"
|
|
||||||
OD_SCLK:
|
|
||||||
0: 800Mhz
|
|
||||||
1: 2100Mhz
|
|
||||||
OD_MCLK:
|
|
||||||
1: 875MHz
|
|
||||||
OD_VDDC_CURVE:
|
|
||||||
0: 800MHz 706mV
|
|
||||||
1: 1450MHz 772mV
|
|
||||||
2: 2100MHz 1143mV
|
|
||||||
OD_RANGE:
|
|
||||||
SCLK: 800Mhz 2150Mhz
|
|
||||||
MCLK: 625Mhz 950Mhz
|
|
||||||
VDDC_CURVE_SCLK[0]: 800Mhz 2150Mhz
|
|
||||||
VDDC_CURVE_VOLT[0]: 750mV 1200mV
|
|
||||||
VDDC_CURVE_SCLK[1]: 800Mhz 2150Mhz
|
|
||||||
VDDC_CURVE_VOLT[1]: 750mV 1200mV
|
|
||||||
VDDC_CURVE_SCLK[2]: 800Mhz 2150Mhz
|
|
||||||
VDDC_CURVE_VOLT[2]: 750mV 1200mV
|
|
||||||
"#;
|
|
||||||
let res = s.trim().parse::<ClockState>();
|
|
||||||
assert_eq!(
|
|
||||||
res,
|
|
||||||
Ok(ClockState {
|
|
||||||
curve_labels: vec![
|
|
||||||
CurvePoint {
|
|
||||||
freq: Frequency {
|
|
||||||
value: 800,
|
|
||||||
unit: "MHz".to_string(),
|
|
||||||
},
|
|
||||||
voltage: Voltage {
|
|
||||||
value: 706,
|
|
||||||
unit: "mV".to_string(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurvePoint {
|
|
||||||
freq: Frequency {
|
|
||||||
value: 1450,
|
|
||||||
unit: "MHz".to_string(),
|
|
||||||
},
|
|
||||||
voltage: Voltage {
|
|
||||||
value: 772,
|
|
||||||
unit: "mV".to_string(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurvePoint {
|
|
||||||
freq: Frequency {
|
|
||||||
value: 2100,
|
|
||||||
unit: "MHz".to_string(),
|
|
||||||
},
|
|
||||||
voltage: Voltage {
|
|
||||||
value: 1143,
|
|
||||||
unit: "mV".to_string(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
engine_label_lowest: Some(Frequency {
|
|
||||||
value: 800,
|
|
||||||
unit: "Mhz".to_string(),
|
|
||||||
}),
|
|
||||||
engine_label_highest: Some(Frequency {
|
|
||||||
value: 2100,
|
|
||||||
unit: "Mhz".to_string(),
|
|
||||||
}),
|
|
||||||
memory_label_lowest: Some(Frequency {
|
|
||||||
value: 875,
|
|
||||||
unit: "MHz".to_string(),
|
|
||||||
}),
|
|
||||||
memory_label_highest: None,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
use amdgpu::hw_mon::HwMon;
|
|
||||||
|
|
||||||
use crate::apply_changes::ApplyChanges;
|
|
||||||
use crate::change_state::ChangeState;
|
|
||||||
use crate::clock_state::{ClockState, Frequency, Voltage};
|
|
||||||
use crate::print_states::PrintStates;
|
|
||||||
use crate::setup_info::SetupInfo;
|
|
||||||
use crate::{Config, VoltageError};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum HardwareModule {
|
|
||||||
Engine,
|
|
||||||
Memory,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for HardwareModule {
|
|
||||||
type Err = VoltageError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"memory" => Ok(HardwareModule::Memory),
|
|
||||||
"engine" => Ok(HardwareModule::Engine),
|
|
||||||
_ => Err(VoltageError::UnknownHardwareModule(s.to_string())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, gumdrop::Options)]
|
|
||||||
pub enum VoltageCommand {
|
|
||||||
SetupInfo(SetupInfo),
|
|
||||||
PrintStates(PrintStates),
|
|
||||||
ChangeState(ChangeState),
|
|
||||||
ApplyChanges(ApplyChanges),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct VoltageManipulator {
|
|
||||||
hw_mon: HwMon,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for VoltageManipulator {
|
|
||||||
type Target = HwMon;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.hw_mon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for VoltageManipulator {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.hw_mon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VoltageManipulator {
|
|
||||||
pub fn wrap(hw_mon: HwMon, _config: &Config) -> Self {
|
|
||||||
Self { hw_mon }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wrap_all(mons: Vec<HwMon>, config: &Config) -> Vec<Self> {
|
|
||||||
mons.into_iter()
|
|
||||||
.map(|mon| Self::wrap(mon, config))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_apply(&self) -> crate::Result<()> {
|
|
||||||
self.device_write("pp_od_clk_voltage", "c")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_state(
|
|
||||||
&self,
|
|
||||||
state_index: u16,
|
|
||||||
freq: Frequency,
|
|
||||||
voltage: Voltage,
|
|
||||||
module: HardwareModule,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
self.device_write(
|
|
||||||
"pp_od_clk_voltage",
|
|
||||||
format!(
|
|
||||||
"{module} {state_index} {freq} {voltage}",
|
|
||||||
state_index = state_index,
|
|
||||||
freq = freq.to_string(),
|
|
||||||
voltage = voltage.to_string(),
|
|
||||||
module = match module {
|
|
||||||
HardwareModule::Engine => "s",
|
|
||||||
HardwareModule::Memory => "m",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clock_states(&self) -> crate::Result<ClockState> {
|
|
||||||
let state = self.device_read("pp_od_clk_voltage")?.parse()?;
|
|
||||||
Ok(state)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
use amdgpu::{utils, AmdGpuError};
|
|
||||||
use amdgpu_config::voltage::ConfigError;
|
|
||||||
|
|
||||||
use crate::change_state::ChangeStateError;
|
|
||||||
use crate::clock_state::ClockStateError;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum VoltageError {
|
|
||||||
#[error("No AMD GPU card was found")]
|
|
||||||
NoAmdGpu,
|
|
||||||
#[error("Unknown hardware module {0:?}")]
|
|
||||||
UnknownHardwareModule(String),
|
|
||||||
#[error("{0}")]
|
|
||||||
AmdGpu(AmdGpuError),
|
|
||||||
#[error("{0}")]
|
|
||||||
Config(#[from] ConfigError),
|
|
||||||
#[error("{0:}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("{0:}")]
|
|
||||||
ClockState(#[from] ClockStateError),
|
|
||||||
#[error("{0:}")]
|
|
||||||
ChangeStateError(#[from] ChangeStateError),
|
|
||||||
#[error("{0:}")]
|
|
||||||
AmdUtils(#[from] utils::AmdGpuError),
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
use amdgpu::utils::ensure_config_dir;
|
|
||||||
use amdgpu_config::voltage::{load_config, Config};
|
|
||||||
use gumdrop::Options;
|
|
||||||
|
|
||||||
use crate::command::VoltageCommand;
|
|
||||||
use crate::error::VoltageError;
|
|
||||||
|
|
||||||
mod apply_changes;
|
|
||||||
mod change_state;
|
|
||||||
mod clock_state;
|
|
||||||
mod command;
|
|
||||||
mod error;
|
|
||||||
mod print_states;
|
|
||||||
mod setup_info;
|
|
||||||
|
|
||||||
pub static DEFAULT_CONFIG_PATH: &str = "/etc/amdfand/voltage.toml";
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, VoltageError>;
|
|
||||||
|
|
||||||
#[derive(gumdrop::Options)]
|
|
||||||
pub struct Opts {
|
|
||||||
#[options(help = "Help message")]
|
|
||||||
help: bool,
|
|
||||||
#[options(help = "Print version")]
|
|
||||||
version: bool,
|
|
||||||
#[options(help = "Config location")]
|
|
||||||
config: Option<String>,
|
|
||||||
#[options(command)]
|
|
||||||
command: Option<command::VoltageCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(config: Config) -> Result<()> {
|
|
||||||
let opts: Opts = Opts::parse_args_default_or_exit();
|
|
||||||
|
|
||||||
if opts.version {
|
|
||||||
println!("amdfand {}", env!("CARGO_PKG_VERSION"));
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
match opts.command {
|
|
||||||
None => {
|
|
||||||
Opts::usage();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Some(VoltageCommand::PrintStates(command)) => print_states::run(command, config),
|
|
||||||
Some(VoltageCommand::SetupInfo(command)) => setup_info::run(command, &config),
|
|
||||||
Some(VoltageCommand::ChangeState(command)) => change_state::run(command, &config),
|
|
||||||
Some(VoltageCommand::ApplyChanges(command)) => apply_changes::run(command, &config),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup() -> Result<(String, Config)> {
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", "DEBUG");
|
|
||||||
}
|
|
||||||
pretty_env_logger::init();
|
|
||||||
ensure_config_dir()?;
|
|
||||||
|
|
||||||
let config_path = Opts::parse_args_default_or_exit()
|
|
||||||
.config
|
|
||||||
.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
|
|
||||||
let config = load_config(&config_path)?;
|
|
||||||
log::set_max_level(config.log_level().as_str().parse().unwrap());
|
|
||||||
Ok((config_path, config))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let (config_path, config) = match setup() {
|
|
||||||
Ok(config) => config,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match run(config) {
|
|
||||||
Ok(()) => Ok(()),
|
|
||||||
Err(e) => {
|
|
||||||
let _config = load_config(&config_path).expect(
|
|
||||||
"Unable to restore automatic voltage control due to unreadable config file",
|
|
||||||
);
|
|
||||||
// panic_handler::restore_automatic(config);
|
|
||||||
log::error!("{}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user