├── .cargo └── config.toml ├── .drone.jsonnet ├── .drone.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.APACHE2.txt ├── LICENSE.md ├── README.md ├── assets ├── config.png ├── logo.png ├── monitoring.png └── settings.png ├── crates ├── amdfan │ ├── Cargo.toml │ ├── LICENSE.md │ └── src │ │ └── main.rs ├── amdfand │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── build.rs │ └── src │ │ ├── change_mode.rs │ │ ├── command.rs │ │ ├── error.rs │ │ ├── main.rs │ │ ├── panic_handler.rs │ │ └── service.rs ├── amdgpu-config │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ └── src │ │ ├── fan.rs │ │ ├── gui.rs │ │ ├── lib.rs │ │ ├── monitor.rs │ │ └── voltage.rs ├── amdgpu │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ └── src │ │ ├── card.rs │ │ ├── error.rs │ │ ├── hw_mon.rs │ │ ├── lib.rs │ │ ├── lock_file.rs │ │ ├── pidfile │ │ ├── helper_cmd.rs │ │ ├── mod.rs │ │ └── ports.rs │ │ ├── temp_input.rs │ │ └── utils.rs ├── amdgui-helper │ ├── .cargo │ │ ├── config │ │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── build.rs │ └── src │ │ └── main.rs ├── amdgui │ ├── .cargo │ │ ├── config │ │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── assets │ │ └── icons │ │ │ ├── html_port-512.png │ │ │ ├── ports.jpg │ │ │ ├── ports2.jpg │ │ │ ├── ports2.png │ │ │ └── vga_port-512.png │ └── src │ │ ├── app.rs │ │ ├── backend.rs │ │ ├── items.rs │ │ ├── items │ │ ├── arrows.rs │ │ ├── h_line.rs │ │ ├── line.rs │ │ ├── marker_shape.rs │ │ ├── plot_image.rs │ │ ├── plot_item.rs │ │ ├── points.rs │ │ ├── polygons.rs │ │ ├── text.rs │ │ ├── v_line.rs │ │ ├── value.rs │ │ └── values.rs │ │ ├── lib.rs │ │ ├── transform.rs │ │ └── widgets │ │ ├── cooling_performance.rs │ │ ├── drag_plot.rs │ │ ├── drag_plot_prepared.rs │ │ ├── edit_temp_config.rs │ │ ├── edit_usage_config.rs │ │ ├── legend.rs │ │ ├── legend_widget.rs │ │ ├── mod.rs │ │ ├── output_widget.rs │ │ ├── outputs_settings.rs │ │ ├── reload_section.rs │ │ ├── temp_config_file.rs │ │ └── usage_config_file.rs ├── amdguid-client │ ├── .cargo │ │ ├── config │ │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── assets │ │ └── icons │ │ │ ├── html_port-512.png │ │ │ ├── ports.jpg │ │ │ ├── ports2.jpg │ │ │ ├── ports2.png │ │ │ └── vga_port-512.png │ └── src │ │ ├── backend.rs │ │ └── main.rs ├── amdmond-lib │ ├── Cargo.toml │ ├── LICENSE.md │ └── src │ │ ├── errors.rs │ │ └── lib.rs ├── amdmond │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── build.rs │ └── src │ │ ├── command.rs │ │ ├── log_file.rs │ │ ├── main.rs │ │ └── watch.rs ├── amdportsd │ ├── Cargo.toml │ ├── LICENSE.md │ ├── build.rs │ └── src │ │ └── main.rs └── amdvold │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── LICENSE.md │ ├── README.md │ ├── assets │ └── enable_voltage_info.txt │ └── src │ ├── apply_changes.rs │ ├── change_state.rs │ ├── clock_state.rs │ ├── command.rs │ ├── error.rs │ ├── main.rs │ ├── print_states.rs │ └── setup_info.rs ├── examples ├── cards_config.toml ├── default_config.toml ├── unsorted_speed_config.toml ├── unsorted_temp_config.toml └── verbose_config.toml ├── recipe.json ├── rustfmt.toml ├── scripts ├── build.sh ├── ci │ ├── build-daemon-pkg │ ├── build-gui-pkg │ ├── cargo-build.sh │ ├── cargo-test.sh │ ├── install-archlinux-dependencies.sh │ └── install-ubuntu-dependencies.sh ├── compile.sh ├── local_install.sh ├── make-aur.sh ├── publish.sh ├── zip-ci.sh └── zip-local.sh └── services ├── amdfand ├── amdfand.service ├── amdgui-helper ├── amdgui-helper.service ├── amdmond ├── amdmond.service ├── amdvold └── amdvold.service /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [registries.crates-io] 2 | protocol = "sparse" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | amdfand.toml 3 | crates/**/*.tar.gz 4 | crates/**/target 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/amdfan", 4 | "crates/amdfand", 5 | "crates/amdgpu", 6 | "crates/amdgpu-config", 7 | "crates/amdgui", 8 | "crates/amdgui-helper", 9 | "crates/amdguid-client", 10 | "crates/amdmond", 11 | "crates/amdmond-lib", 12 | "crates/amdportsd", 13 | "crates/amdvold", 14 | ] 15 | resolver = "2" 16 | 17 | [workspace.dependencies] 18 | egui = { version = "0.31" } 19 | egui_plot = { version = "0.31" } 20 | emath = { version = "0.31" } 21 | epaint = { version = "0.31" } 22 | egui-winit = { version = "0.31" } 23 | winit = { version = "0.30.9", features = ["x11", "wayland"] } 24 | eframe = { version = "0.31" } 25 | tracing = { version = "^0.1.37" } 26 | bytemuck = "1" 27 | chrono = "0.4.19" 28 | crossbeam = "0.8.1" 29 | crossterm = "0.23.2" 30 | csv = "1.1.6" 31 | futures = "0.3" 32 | image = { version = "=0.24.0", default-features = false } 33 | onlyerror = "0" 34 | parking_lot = "=0.12.1" 35 | pidlock = "0.1" 36 | serde = "1.0.137" 37 | sudo = "0.6" 38 | tempdir = "0.3.7" 39 | thiserror = "1.0.30" 40 | tokio = "1.19.2" 41 | tracing-subscriber = "0.3.17" 42 | tui = "0.18.0" 43 | toml = "0.5" 44 | ron = "0.7" 45 | nix = "0.24" 46 | gumdrop = "0.8" 47 | eyra = "0" 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub](https://img.shields.io/github/license/Eraden/amdgpud) 2 | [![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=discord)](https://discord.gg/HXN2QXj3Gv) 3 | [![amdgpud](https://badgen.net/badge/Discord%20Activity/Online/green?icon=buymeacoffee)](https://discord.gg/HXN2QXj3Gv) 4 | 5 | [![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) 6 | 7 | # AMD GPU management tools 8 | 9 | This repository holds couple tools for AMD graphic cards 10 | 11 | * `amdfand` - fan speed daemon (MUSL) 12 | * `amdvold` - voltage and overclocking tool (MUSL) 13 | * `amdmond` - monitor daemon (MUSL) 14 | * `amdguid` - GUI manager (GLIBC) 15 | * `amdgui-helper` - daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files (MUSL) 16 | 17 | For more information please check README each of them. 18 | 19 | ## Roadmap 20 | 21 | * [X] Add support for multiple cards 22 | * Multiple services must recognize card even if there's multiple same version cards is installed 23 | * Support should be by using `--config` option 24 | * [X] CLI for fan config edit 25 | * [ ] CLI for voltage edit 26 | * [X] GUI application using native Rust framework (ex. egui, druid) 27 | 28 | ## License 29 | 30 | This work is dual-licensed under Apache 2.0 and MIT. You can choose between one of them if you use this work. 31 | 32 | ## Supported OS 33 | 34 | Only Linux is supported. 35 | 36 | BSD OS may work if compiled from source, but it wasn't tested. 37 | I also don't know how to enable all amd gpu features on BSD. 38 | 39 | ### Officially supported 40 | 41 | * Arch Linux 42 | * Ubuntu 18.04 43 | * Ubuntu 20.04 44 | 45 | ### Other 46 | 47 | It should be possible to run MUSL programs on any Linux distribution. 48 | 49 | GLIBC applications depends on shared glibc libraries and version of this library MUST match. 50 | 51 | If you have other version you may download linked version and place it in application directory. 52 | 53 | Or you can compile it from source 54 | 55 | #### Compile 56 | 57 | ```bash 58 | ./scripts/build.sh 59 | ``` 60 | 61 | #### Download missing shared libraries 62 | 63 | ##### Check linked versions 64 | 65 | ```bash 66 | ldd ./amdguid 67 | ``` 68 | 69 | Example output: 70 | 71 | ``` 72 | linux-vdso.so.1 (0x00007ffd706df000) 73 | libxcb.so.1 => /usr/lib/libxcb.so.1 (0x00007f4254a50000) 74 | libxcb-render.so.0 => /usr/lib/libxcb-render.so.0 (0x00007f4254a40000) 75 | libxcb-shape.so.0 => /usr/lib/libxcb-shape.so.0 (0x00007f4254a3b000) 76 | libxcb-xfixes.so.0 => /usr/lib/libxcb-xfixes.so.0 (0x00007f4254a31000) 77 | libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f4254a2c000) 78 | libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f4254a11000) 79 | librt.so.1 => /usr/lib/librt.so.1 (0x00007f4254a0a000) 80 | libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007f4254a05000) 81 | libm.so.6 => /usr/lib/libm.so.6 (0x00007f425491d000) 82 | libc.so.6 => /usr/lib/libc.so.6 (0x00007f4254713000) 83 | /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f42556a6000) 84 | libXau.so.6 => /usr/lib/libXau.so.6 (0x00007f425470e000) 85 | libXdmcp.so.6 => /usr/lib/libXdmcp.so.6 (0x00007f4254706000) 86 | ``` 87 | 88 | If anything is missing you may download is and place it side-by-side with binary 89 | 90 | ##### Example: 91 | 92 | ``` 93 | opt/ 94 | ├─ amdguid/ 95 | │ ├─ amdguid 96 | │ ├─ shared_lib_a/ 97 | │ ├─ shared_lib_b/ 98 | usr/ 99 | ├─ bin/ 100 | │ ├─ amdguid 101 | ``` 102 | 103 | Where: 104 | 105 | * `/opt/amdguid/amdguid` is binary file 106 | * `/usr/bin/amdguid` is shell script with following content 107 | 108 | ```bash 109 | #!/usr/bin/env bash 110 | 111 | cd /opt/amdguid 112 | ./amdguid 113 | ``` 114 | -------------------------------------------------------------------------------- /assets/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/assets/config.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/assets/logo.png -------------------------------------------------------------------------------- /assets/monitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/assets/monitoring.png -------------------------------------------------------------------------------- /assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/assets/settings.png -------------------------------------------------------------------------------- /crates/amdfan/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdfan" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "AMDGPU library" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [dependencies] 14 | amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] } 15 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] } 16 | crossbeam = { workspace = true } 17 | crossterm = { workspace = true } 18 | gumdrop = { workspace = true } 19 | ron = { workspace = true } 20 | serde = { workspace = true, features = ["derive"] } 21 | toml = { workspace = true } 22 | tracing = { workspace = true } 23 | tui = { workspace = true, features = [] } 24 | 25 | [dev-dependencies] 26 | amdgpu = { path = "../amdgpu", version = "1.0" } 27 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] } 28 | -------------------------------------------------------------------------------- /crates/amdfan/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdfand/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdfand/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdfand" 3 | version = "1.0.14" 4 | edition = "2018" 5 | description = "AMDGPU fan control service" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | target = "x86_64-unknown-linux-musl" 15 | 16 | [features] 17 | static = ["eyra"] 18 | 19 | [[bin]] 20 | name = "amdfand" 21 | path = "./src/main.rs" 22 | 23 | [dependencies] 24 | amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] } 25 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["fan"] } 26 | eyra = { workspace = true, optional = true } 27 | gumdrop = { workspace = true } 28 | onlyerror = { workspace = true } 29 | ron = { workspace = true } 30 | serde = { workspace = true, features = ["derive"] } 31 | toml = { workspace = true } 32 | tracing = { workspace = true } 33 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 34 | 35 | [dev-dependencies] 36 | amdgpu = { path = "../amdgpu", version = "1.0" } 37 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan"] } 38 | tempdir = { workspace = true } 39 | 40 | [package.metadata.aur] 41 | depends = [] 42 | optdepends = [] 43 | files = [[ 44 | "../../services/amdfand" 45 | ]] 46 | -------------------------------------------------------------------------------- /crates/amdfand/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdfand/README.md: -------------------------------------------------------------------------------- 1 | ![GitHub](https://img.shields.io/github/license/Eraden/amdgpud) 2 | 3 | # AMDGPU Fan control service 4 | 5 | Available commands: 6 | 7 | * `service` - Set fan speed depends on GPU temperature 8 | * `set-automatic` - Switch to GPU automatic fan speed control 9 | * `set-manual` - Switch to GPU manual fan speed control 10 | * `available` - Print available cards 11 | 12 | #### amdfand set-automatic | set-manual [OPTIONS] 13 | 14 | Optional arguments: 15 | 16 | * -h, --help Help message 17 | * -c, --card CARD GPU Card number 18 | 19 | ## Usage 20 | 21 | ```bash 22 | cargo install amdfand 23 | 24 | sudo amdfand monitor # print current temperature, current fan speed, min and max fan speed 25 | sudo amdfand service # check amdgpu temperature and adjust speed from config file 26 | ``` 27 | 28 | ## Config file 29 | 30 | ```toml 31 | # /etc/amdfand/config.toml 32 | log_level = "Error" 33 | temp_input = "temp1_input" 34 | update_rate = 4000 # time between checks in milliseconds 35 | 36 | # GPU temperature to fan speed matrix 37 | [[temp_matrix]] 38 | temp = 4.0 39 | speed = 4.0 40 | 41 | [[temp_matrix]] 42 | temp = 30.0 43 | speed = 33.0 44 | 45 | [[temp_matrix]] 46 | temp = 45.0 47 | speed = 50.0 48 | 49 | [[temp_matrix]] 50 | temp = 60.0 51 | speed = 66.0 52 | 53 | [[temp_matrix]] 54 | temp = 65.0 55 | speed = 69.0 56 | 57 | [[temp_matrix]] 58 | temp = 70.0 59 | speed = 75.0 60 | 61 | [[temp_matrix]] 62 | temp = 75.0 63 | speed = 89.0 64 | 65 | [[temp_matrix]] 66 | temp = 80.0 67 | speed = 100.0 68 | 69 | # GPU usage to fan speed matrix 70 | [[usage_matrix]] 71 | usage = 30.0 72 | speed = 34.0 73 | 74 | [[usage_matrix]] 75 | usage = 65.0 76 | speed = 60.0 77 | ``` 78 | -------------------------------------------------------------------------------- /crates/amdfand/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "static")] 3 | println!("cargo:rustc-link-arg=-nostartfiles"); 4 | } 5 | -------------------------------------------------------------------------------- /crates/amdfand/src/change_mode.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::hw_mon::SysFs; 2 | use amdgpu::utils::hw_mons; 3 | use amdgpu_config::fan::Config; 4 | use gumdrop::Options; 5 | use tracing::error; 6 | 7 | use crate::command::Fan; 8 | use crate::{AmdFanError, FanMode}; 9 | 10 | /// Change card fan mode to either automatic or manual 11 | pub fn run(switcher: Switcher, mode: FanMode, config: Config) -> crate::Result<()> { 12 | let mut hw_mons = Fan::::wrap_all(hw_mons(true)?, &config); 13 | 14 | let cards = match switcher.card { 15 | Some(card_id) => match hw_mons.iter().position(|hw_mon| **hw_mon.card() == card_id) { 16 | Some(card) => vec![hw_mons.remove(card)], 17 | None => { 18 | eprintln!("Card does not exists. Available cards: "); 19 | for hw_mon in hw_mons { 20 | eprintln!(" * {}", *hw_mon.card()); 21 | } 22 | return Err(AmdFanError::NoAmdCardFound); 23 | } 24 | }, 25 | None => hw_mons, 26 | }; 27 | 28 | for hw_mon in cards { 29 | match mode { 30 | FanMode::Automatic => { 31 | if let Err(e) = hw_mon.write_automatic() { 32 | error!("{:?}", e); 33 | } 34 | } 35 | FanMode::Manual => { 36 | if let Err(e) = hw_mon.write_manual() { 37 | error!("{:?}", e); 38 | } 39 | } 40 | } 41 | } 42 | Ok(()) 43 | } 44 | 45 | #[derive(Debug, Options)] 46 | pub struct Switcher { 47 | #[options(help = "Print help message")] 48 | help: bool, 49 | #[options(help = "GPU Card number")] 50 | card: Option, 51 | } 52 | -------------------------------------------------------------------------------- /crates/amdfand/src/error.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::{utils, AmdGpuError}; 2 | use amdgpu_config::fan::ConfigError; 3 | 4 | use crate::command::FanError; 5 | 6 | #[derive(Debug, onlyerror::Error)] 7 | pub enum AmdFanError { 8 | #[error("Vendor is not AMD")] 9 | NotAmdCard, 10 | #[error("No hwmod has been found in sysfs")] 11 | NoHwMonFound, 12 | #[error("No AMD Card has been found in sysfs")] 13 | NoAmdCardFound, 14 | #[error("{0}")] 15 | AmdGpu(#[from] AmdGpuError), 16 | #[error("{0}")] 17 | Fan(#[from] FanError), 18 | #[error("{0}")] 19 | Config(#[from] ConfigError), 20 | #[error("{0:}")] 21 | Io(#[from] std::io::Error), 22 | #[error("{0:}")] 23 | AmdUtils(#[from] utils::AmdGpuError), 24 | } 25 | -------------------------------------------------------------------------------- /crates/amdfand/src/main.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::lock_file::PidLock; 2 | use amdgpu::utils::{ensure_config_dir, hw_mons}; 3 | use amdgpu_config::fan::{load_config, Config, DEFAULT_FAN_CONFIG_PATH}; 4 | use gumdrop::Options; 5 | use tracing::level_filters::LevelFilter; 6 | use tracing::{error, warn}; 7 | use tracing_subscriber::EnvFilter; 8 | 9 | use crate::command::FanCommand; 10 | use crate::error::AmdFanError; 11 | 12 | mod change_mode; 13 | mod command; 14 | mod error; 15 | mod panic_handler; 16 | mod service; 17 | 18 | #[cfg(feature = "static")] 19 | extern crate eyra; 20 | 21 | pub type Result = std::result::Result; 22 | 23 | pub enum FanMode { 24 | Manual, 25 | Automatic, 26 | } 27 | 28 | #[derive(Options)] 29 | pub struct Opts { 30 | #[options(help = "Help message")] 31 | help: bool, 32 | #[options(help = "Print version")] 33 | version: bool, 34 | #[options(help = "Config location")] 35 | config: Option, 36 | #[options( 37 | help = "Pid file name (exp. card1). This should not be path, only file name without extension" 38 | )] 39 | pid_file: Option, 40 | #[options(command)] 41 | command: Option, 42 | } 43 | 44 | static DEFAULT_PID_FILE_NAME: &str = "amdfand"; 45 | 46 | fn run(config: Config) -> Result<()> { 47 | let opts: Opts = Opts::parse_args_default_or_exit(); 48 | 49 | if opts.version { 50 | println!("amdfand {}", env!("CARGO_PKG_VERSION")); 51 | std::process::exit(0); 52 | } 53 | #[allow(deprecated)] 54 | if config.cards().is_some() { 55 | warn!("cards config field is no longer supported"); 56 | } 57 | 58 | match opts.command { 59 | None => run_service(config, opts), 60 | Some(FanCommand::Service(_)) => run_service(config, opts), 61 | Some(FanCommand::SetAutomatic(switcher)) => { 62 | change_mode::run(switcher, FanMode::Automatic, config) 63 | } 64 | Some(FanCommand::SetManual(switcher)) => { 65 | change_mode::run(switcher, FanMode::Manual, config) 66 | } 67 | Some(FanCommand::Available(_)) => { 68 | println!("Available cards"); 69 | hw_mons(false)?.into_iter().for_each(|hw_mon| { 70 | println!( 71 | " * {:6>} - {}", 72 | hw_mon.card(), 73 | hw_mon.name().unwrap_or_default() 74 | ); 75 | }); 76 | Ok(()) 77 | } 78 | } 79 | } 80 | 81 | fn run_service(config: Config, opts: Opts) -> Result<()> { 82 | let mut pid_file = PidLock::new( 83 | "amdfand", 84 | opts.pid_file 85 | .unwrap_or_else(|| String::from(DEFAULT_PID_FILE_NAME)), 86 | )?; 87 | pid_file.acquire()?; 88 | let res = service::run(config); 89 | pid_file.release()?; 90 | res 91 | } 92 | 93 | fn setup() -> Result<(String, Config)> { 94 | if std::env::var("RUST_LOG").is_err() { 95 | std::env::set_var("RUST_LOG", "DEBUG"); 96 | } 97 | ensure_config_dir()?; 98 | 99 | let config_path = Opts::parse_args_default_or_exit() 100 | .config 101 | .unwrap_or_else(|| DEFAULT_FAN_CONFIG_PATH.to_string()); 102 | let config = load_config(&config_path)?; 103 | tracing_subscriber::fmt::fmt() 104 | .with_env_filter(EnvFilter::from_default_env()) 105 | .with_max_level(config.log_level().as_str().parse::().unwrap()) 106 | .init(); 107 | Ok((config_path, config)) 108 | } 109 | 110 | fn main() -> Result<()> { 111 | let (_config_path, config) = match setup() { 112 | Ok(config) => config, 113 | Err(e) => { 114 | error!("{}", e); 115 | std::process::exit(1); 116 | } 117 | }; 118 | match run(config) { 119 | Ok(()) => Ok(()), 120 | Err(e) => { 121 | panic_handler::restore_automatic(); 122 | error!("{}", e); 123 | std::process::exit(1); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/amdfand/src/panic_handler.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::hw_mons; 2 | use tracing::error; 3 | 4 | use crate::command::Fan; 5 | 6 | pub fn restore_automatic() { 7 | for hw in hw_mons(true).unwrap_or_default() { 8 | if let Err(error) = (Fan { 9 | hw_mon: hw, 10 | temp_inputs: vec![], 11 | temp_input: None, 12 | pwm_min: None, 13 | pwm_max: None, 14 | }) 15 | .write_automatic() 16 | { 17 | error!("{}", error); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/amdgpu-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdgpu-config" 3 | version = "1.0.14" 4 | edition = "2021" 5 | description = "Subcomponent of AMDGPU tools" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | 11 | [lib] 12 | name = "amdgpu_config" 13 | path = "./src/lib.rs" 14 | 15 | [features] 16 | fan = [] 17 | voltage = [] 18 | monitor = [] 19 | gui = [] 20 | 21 | [dependencies] 22 | amdgpu = { path = "../amdgpu", version = "1.0.11", features = ["gui-helper"] } 23 | csv = { workspace = true } 24 | gumdrop = { workspace = true } 25 | serde = { workspace = true, features = ["derive"] } 26 | thiserror = { workspace = true } 27 | toml = { workspace = true } 28 | tracing = { workspace = true } 29 | 30 | [dev-dependencies] 31 | amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] } 32 | -------------------------------------------------------------------------------- /crates/amdgpu-config/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdgpu-config/README.md: -------------------------------------------------------------------------------- 1 | # amdgpu-config 2 | 3 | This crates holds config files for `amdfand`, `amdvold` and `amdmond`. 4 | 5 | For more information please check those services. -------------------------------------------------------------------------------- /crates/amdgpu-config/src/gui.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::ensure_config; 2 | use amdgpu::LogLevel; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub enum ConfigError { 6 | #[error("{0}")] 7 | Io(#[from] std::io::Error), 8 | } 9 | 10 | #[derive(serde::Deserialize, serde::Serialize)] 11 | pub struct Config { 12 | /// Minimal log level 13 | log_level: LogLevel, 14 | } 15 | 16 | impl Default for Config { 17 | fn default() -> Self { 18 | Self { 19 | log_level: LogLevel::Error, 20 | } 21 | } 22 | } 23 | 24 | impl Config { 25 | pub fn log_level(&self) -> LogLevel { 26 | self.log_level 27 | } 28 | } 29 | 30 | pub fn load_config(config_path: &str) -> Result { 31 | let config: Config = ensure_config::(config_path)?; 32 | 33 | Ok(config) 34 | } 35 | 36 | #[cfg(test)] 37 | mod serde_tests { 38 | use crate::gui::Config; 39 | 40 | #[test] 41 | fn serialize() { 42 | let res = toml::to_string(&Config::default()); 43 | assert!(res.is_ok()); 44 | } 45 | 46 | #[test] 47 | fn deserialize() { 48 | let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); 49 | assert!(res.is_ok()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/amdgpu-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "fan")] 2 | pub mod fan; 3 | #[cfg(feature = "gui")] 4 | pub mod gui; 5 | #[cfg(feature = "monitor")] 6 | pub mod monitor; 7 | #[cfg(feature = "voltage")] 8 | pub mod voltage; 9 | 10 | /// pulse width modulation fan level (0-255) 11 | pub static PULSE_WIDTH_MODULATION: &str = "pwm1"; 12 | -------------------------------------------------------------------------------- /crates/amdgpu-config/src/monitor.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::ensure_config; 2 | use amdgpu::LogLevel; 3 | use serde::{Deserialize, Serialize}; 4 | use tracing::warn; 5 | 6 | pub static DEFAULT_MONITOR_CONFIG_PATH: &str = "/etc/amdfand/monitor.toml"; 7 | 8 | #[derive(Debug, Deserialize, Serialize)] 9 | pub struct Config { 10 | /// Minimal log level 11 | log_level: LogLevel, 12 | /// Duration in milliseconds 13 | interval: u64, 14 | } 15 | 16 | #[derive(Debug, thiserror::Error)] 17 | pub enum ConfigError { 18 | #[error("{0}")] 19 | Io(#[from] std::io::Error), 20 | } 21 | 22 | impl ConfigError { 23 | pub fn into_io(self) -> std::io::Error { 24 | match self { 25 | ConfigError::Io(io) => io, 26 | } 27 | } 28 | } 29 | 30 | impl Default for Config { 31 | fn default() -> Self { 32 | Self { 33 | log_level: LogLevel::Error, 34 | interval: 5000, 35 | } 36 | } 37 | } 38 | 39 | impl Config { 40 | pub fn log_level(&self) -> LogLevel { 41 | self.log_level 42 | } 43 | 44 | pub fn interval(&self) -> u64 { 45 | self.interval 46 | } 47 | } 48 | 49 | pub fn load_config(config_path: &str) -> Result { 50 | let mut config: Config = ensure_config::(config_path)?; 51 | 52 | if config.interval < 100 { 53 | warn!( 54 | "Minimal interval is 100ms, overriding {}ms", 55 | config.interval 56 | ); 57 | config.interval = 100; 58 | } 59 | 60 | Ok(config) 61 | } 62 | 63 | #[cfg(test)] 64 | mod serde_tests { 65 | use crate::monitor::Config; 66 | 67 | #[test] 68 | fn serialize() { 69 | let res = toml::to_string(&Config::default()); 70 | assert!(res.is_ok()); 71 | } 72 | 73 | #[test] 74 | fn deserialize() { 75 | let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); 76 | assert!(res.is_ok()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /crates/amdgpu-config/src/voltage.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::ensure_config; 2 | use amdgpu::LogLevel; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub enum ConfigError { 6 | #[error("{0}")] 7 | Io(#[from] std::io::Error), 8 | } 9 | 10 | #[derive(serde::Deserialize, serde::Serialize)] 11 | pub struct Config { 12 | log_level: LogLevel, 13 | } 14 | 15 | impl Default for Config { 16 | fn default() -> Self { 17 | Self { 18 | log_level: LogLevel::Error, 19 | } 20 | } 21 | } 22 | 23 | impl Config { 24 | pub fn log_level(&self) -> LogLevel { 25 | self.log_level 26 | } 27 | } 28 | 29 | pub fn load_config(config_path: &str) -> Result { 30 | ensure_config::(config_path) 31 | } 32 | 33 | #[cfg(test)] 34 | mod serde_tests { 35 | use crate::voltage::Config; 36 | 37 | #[test] 38 | fn serialize() { 39 | let res = toml::to_string(&Config::default()); 40 | assert!(res.is_ok()); 41 | } 42 | 43 | #[test] 44 | fn deserialize() { 45 | let res = toml::from_str::(&toml::to_string(&Config::default()).unwrap()); 46 | assert!(res.is_ok()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/amdgpu/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdgpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdgpu" 3 | version = "1.0.12" 4 | edition = "2018" 5 | description = "Subcomponent of AMDGPU fan control service" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | 11 | [features] 12 | gui-helper = [] 13 | 14 | [dependencies] 15 | egui-winit = { workspace = true } 16 | gumdrop = { workspace = true } 17 | nix = { workspace = true } 18 | pidlock = { workspace = true } 19 | ron = { workspace = true } 20 | serde = { workspace = true, features = ["derive"] } 21 | thiserror = { workspace = true } 22 | toml = { workspace = true } 23 | tracing = { workspace = true } 24 | -------------------------------------------------------------------------------- /crates/amdgpu/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdgpu/README.md: -------------------------------------------------------------------------------- 1 | # amdgpu-config 2 | 3 | This is shared data for `amdfand`, `amdvold` and `amdmond`. 4 | 5 | For more information please check those services. -------------------------------------------------------------------------------- /crates/amdgpu/src/card.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::AmdGpuError; 4 | 5 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 6 | pub struct Card(pub u32); 7 | 8 | impl std::fmt::Display for Card { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | f.write_str(&format!("card{}", self.0)) 11 | } 12 | } 13 | 14 | impl std::str::FromStr for Card { 15 | type Err = AmdGpuError; 16 | 17 | fn from_str(value: &str) -> Result { 18 | if !value.starts_with("card") { 19 | return Err(AmdGpuError::CardInvalidPrefix); 20 | } 21 | if value.len() < 5 { 22 | return Err(AmdGpuError::CardInputTooShort); 23 | } 24 | value[4..] 25 | .parse::() 26 | .map_err(|e| AmdGpuError::CardInvalidSuffix(format!("{:?}", e))) 27 | .map(Card) 28 | } 29 | } 30 | 31 | impl<'de> Deserialize<'de> for Card { 32 | fn deserialize(deserializer: D) -> Result 33 | where 34 | D: serde::Deserializer<'de>, 35 | { 36 | use serde::de::{self, Visitor}; 37 | 38 | struct CardVisitor; 39 | 40 | impl<'de> Visitor<'de> for CardVisitor { 41 | type Value = u32; 42 | 43 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 44 | formatter.write_str("must have format cardX") 45 | } 46 | 47 | fn visit_str(self, value: &str) -> Result 48 | where 49 | E: de::Error, 50 | { 51 | match value.parse::() { 52 | Ok(card) => Ok(*card), 53 | Err(AmdGpuError::CardInvalidPrefix) => { 54 | Err(E::custom(format!("expect cardX but got {}", value))) 55 | } 56 | Err(AmdGpuError::CardInvalidSuffix(s)) => Err(E::custom(s)), 57 | Err(AmdGpuError::CardInputTooShort) => Err(E::custom(format!( 58 | "{:?} must have at least 5 characters", 59 | value 60 | ))), 61 | _ => unreachable!(), 62 | } 63 | } 64 | } 65 | deserializer.deserialize_str(CardVisitor).map(Card) 66 | } 67 | } 68 | 69 | impl serde::Serialize for Card { 70 | fn serialize(&self, serializer: S) -> Result 71 | where 72 | S: serde::Serializer, 73 | { 74 | serializer.serialize_str(&self.to_string()) 75 | } 76 | } 77 | 78 | impl std::ops::Deref for Card { 79 | type Target = u32; 80 | 81 | fn deref(&self) -> &Self::Target { 82 | &self.0 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/amdgpu/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter}; 2 | 3 | use pidlock::PidlockError; 4 | 5 | use crate::lock_file::LockFileError; 6 | #[cfg(feature = "gui-helper")] 7 | use crate::pidfile::helper_cmd::GuiHelperError; 8 | use crate::pidfile::ports::PortsError; 9 | 10 | pub struct IoFailure { 11 | pub io: std::io::Error, 12 | pub path: std::path::PathBuf, 13 | } 14 | 15 | impl std::error::Error for IoFailure {} 16 | 17 | impl Debug for IoFailure { 18 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 19 | f.write_fmt(format_args!( 20 | "File system error for {:?}. {:?}", 21 | self.path, self.io 22 | )) 23 | } 24 | } 25 | 26 | impl Display for IoFailure { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 28 | f.write_fmt(format_args!("{:?}", self)) 29 | } 30 | } 31 | 32 | #[derive(Debug, thiserror::Error)] 33 | pub enum AmdGpuError { 34 | #[error("Card must starts with `card`.")] 35 | CardInvalidPrefix, 36 | #[error("Card must start with `card` and ends with a number. The given name is too short.")] 37 | CardInputTooShort, 38 | #[error("Value after `card` is invalid {0:}")] 39 | CardInvalidSuffix(String), 40 | #[error("Invalid temperature input")] 41 | InvalidTempInput(String), 42 | #[error("Unable to read GPU vendor")] 43 | FailedReadVendor, 44 | #[error("{0}")] 45 | Io(#[from] IoFailure), 46 | #[error("Card does not have hwmon")] 47 | NoAmdHwMon, 48 | #[error("{0:?}")] 49 | PidFile(#[from] PidLockError), 50 | #[cfg(feature = "gui-helper")] 51 | #[error("{0:?}")] 52 | GuiHelper(#[from] GuiHelperError), 53 | #[error("{0:?}")] 54 | Ports(#[from] PortsError), 55 | #[error("{0:?}")] 56 | LockFile(#[from] LockFileError), 57 | } 58 | 59 | #[derive(Debug, thiserror::Error)] 60 | pub enum PidLockError { 61 | #[error("A lock already exists")] 62 | LockExists, 63 | #[error("An operation was attempted in the wrong state, e.g. releasing before acquiring.")] 64 | InvalidState, 65 | } 66 | 67 | impl From for PidLockError { 68 | fn from(e: PidlockError) -> Self { 69 | match e { 70 | pidlock::PidlockError::LockExists => PidLockError::LockExists, 71 | pidlock::PidlockError::InvalidState => PidLockError::InvalidState, 72 | } 73 | } 74 | } 75 | 76 | impl PartialEq for AmdGpuError { 77 | fn eq(&self, other: &Self) -> bool { 78 | use AmdGpuError::*; 79 | match (self, other) { 80 | (CardInvalidPrefix, CardInvalidPrefix) => true, 81 | (CardInputTooShort, CardInputTooShort) => true, 82 | (CardInvalidSuffix(a), CardInvalidSuffix(b)) => a == b, 83 | (InvalidTempInput(a), InvalidTempInput(b)) => a == b, 84 | (FailedReadVendor, FailedReadVendor) => true, 85 | (NoAmdHwMon, NoAmdHwMon) => true, 86 | (Io(a), Io(b)) => a.io.kind() == b.io.kind(), 87 | _ => false, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /crates/amdgpu/src/hw_mon.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use tracing::error; 4 | 5 | use crate::{utils, AmdGpuError, Card, IoFailure, ROOT_DIR}; 6 | 7 | #[derive(Debug)] 8 | pub struct HwMonName(pub String); 9 | 10 | impl std::ops::Deref for HwMonName { 11 | type Target = String; 12 | 13 | fn deref(&self) -> &Self::Target { 14 | &self.0 15 | } 16 | } 17 | 18 | pub trait RootPath { 19 | fn root_dir(&self) -> PathBuf; 20 | fn device_dir(&self, card: &Card) -> PathBuf; 21 | fn mon_dir(&self, card: &Card, name: &HwMonName) -> PathBuf; 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct SysFs; 26 | 27 | impl RootPath for SysFs { 28 | fn root_dir(&self) -> PathBuf { 29 | PathBuf::new().join(ROOT_DIR) 30 | } 31 | 32 | fn device_dir(&self, card: &Card) -> PathBuf { 33 | self.root_dir().join(card.to_string()).join("device") 34 | } 35 | 36 | fn mon_dir(&self, card: &Card, name: &HwMonName) -> PathBuf { 37 | self.device_dir(card).join("hwmon").join(name.as_str()) 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct HwMon { 43 | /// HW MOD cord (ex. card0) 44 | pub card: Card, 45 | /// MW MOD name (ex. hwmod0) 46 | pub name: HwMonName, 47 | root: Root, 48 | } 49 | 50 | impl HwMon { 51 | pub fn new(card: &Card, name: HwMonName, root: Root) -> Self { 52 | Self { 53 | card: *card, 54 | name, 55 | root, 56 | } 57 | } 58 | 59 | #[inline] 60 | pub fn card(&self) -> &Card { 61 | &self.card 62 | } 63 | 64 | #[inline] 65 | pub fn name(&self) -> utils::Result { 66 | self.hw_mon_read("name") 67 | } 68 | 69 | /// GPU usage percent 70 | pub fn gpu_usage(&self) -> utils::Result { 71 | Ok(self.read_gpu_usage()? as f64) 72 | } 73 | 74 | pub fn read_gpu_usage(&self) -> utils::Result { 75 | let path = self.root.device_dir(self.card()).join("gpu_busy_percent"); 76 | let value = utils::read_to_string(path)? 77 | .trim() 78 | .parse() 79 | .unwrap_or_else(|e| { 80 | error!("{e}"); 81 | 0 82 | }); 83 | Ok(value) 84 | } 85 | 86 | #[inline] 87 | pub fn is_amd(&self) -> bool { 88 | self.device_read("vendor") 89 | .map_err(|_| AmdGpuError::FailedReadVendor) 90 | .map(|vendor| vendor.trim() == "0x1002") 91 | .unwrap_or_default() 92 | } 93 | 94 | #[inline] 95 | pub fn name_is_amd(&self) -> bool { 96 | self.name().ok().filter(|s| s.trim() == "amdgpu").is_some() 97 | } 98 | 99 | fn mon_file_path(&self, name: &str) -> PathBuf { 100 | self.mon_dir().join(name) 101 | } 102 | 103 | pub fn device_dir(&self) -> PathBuf { 104 | self.root.device_dir(self.card()) 105 | } 106 | 107 | pub fn mon_dir(&self) -> PathBuf { 108 | self.root.mon_dir(self.card(), &self.name) 109 | } 110 | 111 | #[inline] 112 | pub fn value_or(&self, name: &str, fallback: R) -> R { 113 | self.hw_mon_read(name) 114 | .ok() 115 | .and_then(|s| s.parse().ok()) 116 | .unwrap_or(fallback) 117 | } 118 | 119 | pub fn hw_mon_read(&self, name: &str) -> utils::Result { 120 | utils::read_to_string(self.mon_file_path(name)).map(|s| String::from(s.trim())) 121 | } 122 | 123 | pub fn device_read(&self, name: &str) -> utils::Result { 124 | utils::read_to_string(self.device_dir().join(name)).map(|s| String::from(s.trim())) 125 | } 126 | 127 | pub fn hw_mon_write(&self, name: &str, value: u64) -> utils::Result<()> { 128 | utils::write(self.mon_file_path(name), format!("{}", value))?; 129 | Ok(()) 130 | } 131 | 132 | pub fn device_write>(&self, name: &str, value: C) -> utils::Result<()> { 133 | utils::write(self.device_dir().join(name), value)?; 134 | Ok(()) 135 | } 136 | } 137 | 138 | #[inline] 139 | fn hw_mon_dirs_path(card: &Card) -> PathBuf { 140 | PathBuf::new() 141 | .join(ROOT_DIR) 142 | .join(card.to_string()) 143 | .join("device") 144 | .join("hwmon") 145 | } 146 | 147 | pub fn open_hw_mon(card: Card) -> crate::Result { 148 | let read_path = hw_mon_dirs_path(&card); 149 | let entries = std::fs::read_dir(&read_path).map_err(|io| IoFailure { 150 | io, 151 | path: read_path, 152 | })?; 153 | let name = entries 154 | .filter_map(|entry| entry.ok()) 155 | .filter_map(|entry| { 156 | entry 157 | .file_name() 158 | .as_os_str() 159 | .to_str() 160 | .filter(|name| name.starts_with("hwmon")) 161 | .map(String::from) 162 | .map(HwMonName) 163 | }) 164 | .take(1) 165 | .last() 166 | .ok_or(AmdGpuError::NoAmdHwMon)?; 167 | Ok(HwMon::new(&card, name, SysFs)) 168 | } 169 | -------------------------------------------------------------------------------- /crates/amdgpu/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use card::*; 2 | pub use error::*; 3 | use serde::{Deserialize, Serialize}; 4 | pub use temp_input::*; 5 | 6 | mod card; 7 | mod error; 8 | #[cfg(feature = "gui-helper")] 9 | pub mod hw_mon; 10 | pub mod lock_file; 11 | pub mod pidfile; 12 | mod temp_input; 13 | pub mod utils; 14 | 15 | pub static CONFIG_DIR: &str = "/etc/amdfand"; 16 | 17 | pub static ROOT_DIR: &str = "/sys/class/drm"; 18 | pub static HW_MON_DIR: &str = "hwmon"; 19 | 20 | /// pulse width modulation fan control minimum level (0) 21 | pub static PULSE_WIDTH_MODULATION_MIN: &str = "pwm1_min"; 22 | 23 | /// pulse width modulation fan control maximum level (255) 24 | pub static PULSE_WIDTH_MODULATION_MAX: &str = "pwm1_max"; 25 | 26 | /// pulse width modulation fan level (0-255) 27 | pub static PULSE_WIDTH_MODULATION: &str = "pwm1"; 28 | 29 | /// pulse width modulation fan control method (0: no fan speed control, 1: 30 | /// manual fan speed control using pwm interface, 2: automatic fan speed 31 | /// control) 32 | pub static PULSE_WIDTH_MODULATION_MODE: &str = "pwm1_enable"; 33 | 34 | // static PULSE_WIDTH_MODULATION_DISABLED: &str = "0"; 35 | pub static PULSE_WIDTH_MODULATION_MANUAL: &str = "1"; 36 | pub static PULSE_WIDTH_MODULATION_AUTO: &str = "2"; 37 | 38 | static mut RELOAD_CONFIG: bool = false; 39 | 40 | extern "C" fn sig_reload(_n: i32) { 41 | unsafe { 42 | RELOAD_CONFIG = true; 43 | }; 44 | } 45 | 46 | /// Listen for SIGHUP signal. This signal is used to reload config 47 | pub fn listen_unix_signal() { 48 | use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal}; 49 | unsafe { 50 | let handler: SigHandler = SigHandler::Handler(sig_reload); 51 | let action = SigAction::new(handler, SaFlags::SA_NOCLDWAIT, SigSet::empty()); 52 | sigaction(Signal::SIGHUP, &action).expect("Failed to mount action handler"); 53 | }; 54 | } 55 | 56 | /// Check if application received SIGHUP and must reload config file 57 | #[inline(always)] 58 | pub fn is_reload_required() -> bool { 59 | unsafe { RELOAD_CONFIG } 60 | } 61 | 62 | /// Reset reload config flag 63 | #[inline(always)] 64 | pub fn config_reloaded() { 65 | unsafe { 66 | RELOAD_CONFIG = false; 67 | } 68 | } 69 | 70 | pub type Result = std::result::Result; 71 | 72 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 73 | pub enum LogLevel { 74 | /// A level lower than all log levels. 75 | Off, 76 | /// Corresponds to the `Error` log level. 77 | Error, 78 | /// Corresponds to the `Warn` log level. 79 | Warn, 80 | /// Corresponds to the `Info` log level. 81 | Info, 82 | /// Corresponds to the `Debug` log level. 83 | Debug, 84 | /// Corresponds to the `Trace` log level. 85 | Trace, 86 | } 87 | 88 | impl LogLevel { 89 | pub fn as_str(&self) -> &str { 90 | match self { 91 | LogLevel::Off => "OFF", 92 | LogLevel::Error => "ERROR", 93 | LogLevel::Warn => "WARN", 94 | LogLevel::Info => "INFO", 95 | LogLevel::Debug => "DEBUG", 96 | LogLevel::Trace => "TRACE", 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /crates/amdgpu/src/pidfile/helper_cmd.rs: -------------------------------------------------------------------------------- 1 | //! AMD GUI helper communication toolkit 2 | 3 | use std::io::{Read, Write}; 4 | use std::os::unix::net::UnixStream; 5 | 6 | use tracing::info; 7 | 8 | use crate::pidfile::{Pid, PidResponse}; 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | pub enum GuiHelperError { 12 | #[error("GUI Helper socket file not found. Is service running?")] 13 | NoSockFile, 14 | #[error("Failed to connect to /var/lib/amdfand/helper.sock. {0}")] 15 | UnableToConnect(#[from] std::io::Error), 16 | #[error("Failed to service helper command. {0}")] 17 | Serialize(#[from] ron::Error), 18 | } 19 | 20 | #[derive(Debug, serde::Deserialize, serde::Serialize)] 21 | pub enum Command { 22 | ReloadConfig { pid: Pid }, 23 | FanServices, 24 | SaveFanConfig { path: String, content: String }, 25 | } 26 | 27 | #[derive(Debug, serde::Deserialize, serde::Serialize)] 28 | pub enum Response { 29 | NoOp, 30 | Services(Vec), 31 | ConfigFileSaved, 32 | ConfigFileSaveFailed(String), 33 | } 34 | 35 | impl PidResponse for Response { 36 | fn kill_response() -> Self { 37 | Self::NoOp 38 | } 39 | } 40 | 41 | pub fn sock_file() -> std::path::PathBuf { 42 | std::path::Path::new("/tmp").join("amdgui-helper.sock") 43 | } 44 | 45 | pub fn send_command(cmd: Command) -> crate::Result { 46 | let sock_path = sock_file(); 47 | 48 | if !sock_path.exists() { 49 | return Err(GuiHelperError::NoSockFile.into()); 50 | } 51 | 52 | let mut stream = UnixStream::connect(&sock_path).map_err(GuiHelperError::UnableToConnect)?; 53 | let s = ron::to_string(&cmd).map_err(GuiHelperError::Serialize)?; 54 | if stream.write_all(format!("{}\n", s).as_bytes()).is_ok() { 55 | info!("Command send"); 56 | } 57 | 58 | let res: Response = { 59 | let mut s = String::with_capacity(100); 60 | let _ = stream.read_to_string(&mut s); 61 | ron::from_str(&s).map_err(GuiHelperError::Serialize)? 62 | }; 63 | 64 | Ok(res) 65 | } 66 | -------------------------------------------------------------------------------- /crates/amdgpu/src/pidfile/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::io::{Read, Write}; 3 | use std::marker::PhantomData; 4 | use std::net::Shutdown; 5 | use std::ops::Deref; 6 | use std::os::unix::net::UnixStream; 7 | 8 | use serde::de::DeserializeOwned; 9 | use serde::Serialize; 10 | use tracing::{error, info, warn}; 11 | 12 | pub mod helper_cmd; 13 | pub mod ports; 14 | 15 | #[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] 16 | #[serde(transparent)] 17 | pub struct Pid(pub i32); 18 | 19 | impl Deref for Pid { 20 | type Target = i32; 21 | 22 | fn deref(&self) -> &Self::Target { 23 | &self.0 24 | } 25 | } 26 | 27 | pub fn handle_connection(stream: UnixStream, handle_command: HandleCmd) 28 | where 29 | HandleCmd: FnOnce(Service, Cmd) + Copy, 30 | Cmd: DeserializeOwned + Serialize + Debug, 31 | Res: DeserializeOwned + Serialize + Debug + PidResponse, 32 | { 33 | let mut service = Service::::new(stream); 34 | 35 | let command = match service.read_command() { 36 | Some(s) => s, 37 | _ => return service.kill(), 38 | }; 39 | 40 | info!("Incoming {:?}", command); 41 | let cmd = match ron::from_str::(command.trim()) { 42 | Ok(cmd) => cmd, 43 | Err(e) => { 44 | warn!("Invalid message {:?}. {:?}", command, e); 45 | return service.kill(); 46 | } 47 | }; 48 | handle_command(service, cmd); 49 | } 50 | 51 | pub trait PidResponse: Sized { 52 | fn kill_response() -> Self; 53 | } 54 | 55 | pub struct Service(UnixStream, PhantomData) 56 | where 57 | Response: serde::Serialize + Debug + PidResponse; 58 | 59 | impl Service 60 | where 61 | Response: serde::Serialize + Debug + PidResponse, 62 | { 63 | pub fn new(file: UnixStream) -> Self { 64 | Self(file, Default::default()) 65 | } 66 | 67 | /// Serialize and send command 68 | pub fn write_response(&mut self, res: Response) { 69 | write_response(&mut self.0, res) 70 | } 71 | 72 | /// Read from `.sock` file new line separated commands 73 | pub fn read_command(&mut self) -> Option { 74 | read_command(&mut self.0) 75 | } 76 | 77 | /// Close connection with no operation response 78 | pub fn kill(mut self) { 79 | self.write_response(Response::kill_response()); 80 | self.close(); 81 | } 82 | 83 | pub fn close(self) { 84 | let _ = self.0.shutdown(Shutdown::Both); 85 | } 86 | } 87 | 88 | /// Serialize and send command 89 | pub fn write_response(file: &mut UnixStream, res: Response) 90 | where 91 | Response: serde::Serialize + Debug, 92 | { 93 | match ron::to_string(&res) { 94 | Ok(buffer) => match file.write_all(buffer.as_bytes()) { 95 | Ok(_) => { 96 | info!("Response successfully written") 97 | } 98 | Err(e) => warn!("Failed to write response. {:?}", e), 99 | }, 100 | Err(e) => { 101 | warn!("Failed to serialize response {:?}. {:?}", res, e) 102 | } 103 | } 104 | } 105 | 106 | /// Read from `.sock` file new line separated commands 107 | pub fn read_command(file: &mut UnixStream) -> Option { 108 | let mut command = String::with_capacity(100); 109 | info!("Reading stream..."); 110 | read_line(file, &mut command); 111 | if command.is_empty() { 112 | return None; 113 | } 114 | Some(command) 115 | } 116 | 117 | pub fn read_line(stream: &mut UnixStream, command: &mut String) { 118 | let mut buffer = [0]; 119 | while stream.read_exact(&mut buffer).is_ok() { 120 | if buffer[0] == b'\n' { 121 | break; 122 | } 123 | match std::str::from_utf8(&buffer) { 124 | Ok(s) => { 125 | command.push_str(s); 126 | } 127 | Err(e) => { 128 | error!("Failed to read from client. {:?}", e); 129 | let _ = stream.shutdown(Shutdown::Both); 130 | continue; 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /crates/amdgpu/src/temp_input.rs: -------------------------------------------------------------------------------- 1 | use serde::Serializer; 2 | use tracing::error; 3 | 4 | use crate::AmdGpuError; 5 | 6 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 7 | pub struct TempInput(pub u16); 8 | 9 | impl TempInput { 10 | pub fn as_string(&self) -> String { 11 | format!("temp{}_input", self.0) 12 | } 13 | } 14 | 15 | impl std::str::FromStr for TempInput { 16 | type Err = AmdGpuError; 17 | 18 | fn from_str(s: &str) -> Result { 19 | if s.starts_with("temp") && s.ends_with("_input") { 20 | let mut buffer = String::with_capacity(4); 21 | for c in s[4..].chars() { 22 | if c.is_numeric() { 23 | buffer.push(c); 24 | } else if buffer.is_empty() { 25 | return Err(AmdGpuError::InvalidTempInput(s.to_string())); 26 | } 27 | } 28 | buffer 29 | .parse() 30 | .map_err(|e| { 31 | error!("Temp input error {:?}", e); 32 | AmdGpuError::InvalidTempInput(s.to_string()) 33 | }) 34 | .map(Self) 35 | } else { 36 | Err(AmdGpuError::InvalidTempInput(s.to_string())) 37 | } 38 | } 39 | } 40 | 41 | impl serde::Serialize for TempInput { 42 | fn serialize(&self, serializer: S) -> Result 43 | where 44 | S: Serializer, 45 | { 46 | serializer.serialize_str(&self.as_string()) 47 | } 48 | } 49 | 50 | impl<'de> serde::Deserialize<'de> for TempInput { 51 | fn deserialize(deserializer: D) -> Result 52 | where 53 | D: serde::Deserializer<'de>, 54 | { 55 | use serde::de::{self, Visitor}; 56 | 57 | struct TempInputVisitor; 58 | 59 | impl<'de> Visitor<'de> for TempInputVisitor { 60 | type Value = u16; 61 | 62 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 63 | formatter.write_str("must have format cardX") 64 | } 65 | 66 | fn visit_str(self, value: &str) -> Result 67 | where 68 | E: de::Error, 69 | { 70 | match value.parse::() { 71 | Ok(temp) => Ok(temp.0), 72 | _ => unreachable!(), 73 | } 74 | } 75 | } 76 | deserializer 77 | .deserialize_str(TempInputVisitor) 78 | .map(TempInput) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /crates/amdgpu/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io::ErrorKind; 2 | 3 | use tracing::{error, info}; 4 | 5 | use crate::hw_mon::{HwMon, RootPath}; 6 | use crate::{hw_mon, Card, CONFIG_DIR, ROOT_DIR}; 7 | 8 | pub type Result = std::result::Result; 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | pub enum AmdGpuError { 12 | #[error("Write to {path:?} failed. {io}")] 13 | Write { io: std::io::Error, path: String }, 14 | #[error("Read from {path:?} failed. {io}")] 15 | Read { io: std::io::Error, path: String }, 16 | #[error("Create path {path:?} failed. {io}")] 17 | CreatePath { io: std::io::Error, path: String }, 18 | #[error("Failed to read directory {path:?}. {io}")] 19 | ReadDir { io: std::io::Error, path: String }, 20 | #[error("File {0:?} does not exists")] 21 | FileNotFound(String), 22 | } 23 | 24 | pub fn read_to_string>(path: P) -> Result { 25 | std::fs::read_to_string(&path).map_err(|io| { 26 | if io.kind() == ErrorKind::NotFound { 27 | AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default()) 28 | } else { 29 | AmdGpuError::Read { 30 | io, 31 | path: path.as_ref().to_str().map(String::from).unwrap_or_default(), 32 | } 33 | } 34 | }) 35 | } 36 | 37 | pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { 38 | std::fs::write(&path, contents).map_err(|io| { 39 | if io.kind() == ErrorKind::NotFound { 40 | AmdGpuError::FileNotFound(path.as_ref().to_str().map(String::from).unwrap_or_default()) 41 | } else { 42 | AmdGpuError::Write { 43 | io, 44 | path: path.as_ref().to_str().map(String::from).unwrap_or_default(), 45 | } 46 | } 47 | }) 48 | } 49 | 50 | /// linear mapping from the xrange to the yrange 51 | pub fn linear_map(x: f64, x1: f64, x2: f64, y1: f64, y2: f64) -> f64 { 52 | let m = (y2 - y1) / (x2 - x1); 53 | m * (x - x1) + y1 54 | } 55 | 56 | /// Read all available graphic cards from direct rendering manager 57 | pub fn read_cards() -> Result> { 58 | Ok(std::fs::read_dir(ROOT_DIR) 59 | .map_err(|io| AmdGpuError::ReadDir { 60 | io, 61 | path: ROOT_DIR.into(), 62 | })? 63 | .filter_map(|entry| entry.ok()) 64 | .filter_map(|entry| entry.file_name().as_os_str().to_str().map(String::from)) 65 | .filter_map(|file_name| file_name.parse::().ok()) 66 | .collect()) 67 | } 68 | 69 | /// Wrap cards in HW Mon manipulator and 70 | /// filter cards so only amd and listed in config cards are accessible 71 | pub fn hw_mons(filter: bool) -> Result> { 72 | Ok(read_cards()? 73 | .into_iter() 74 | .flat_map(|card| { 75 | info!("opening hw mon for {:?}", card); 76 | hw_mon::open_hw_mon(card) 77 | }) 78 | .filter(|hw_mon| { 79 | !filter || { 80 | info!("is vendor ok? {}", hw_mon.is_amd()); 81 | hw_mon.is_amd() 82 | } 83 | }) 84 | .filter(|hw_mon| { 85 | !filter || { 86 | info!("is hwmon name ok? {}", hw_mon.name_is_amd()); 87 | hw_mon.name_is_amd() 88 | } 89 | }) 90 | .collect()) 91 | } 92 | 93 | /// Try to read from config file or create new config file. 94 | /// Create only if it does not exists, malformed file will raise error 95 | pub fn ensure_config(config_path: P) -> std::result::Result 96 | where 97 | Config: serde::Serialize + serde::de::DeserializeOwned + Default + Sized, 98 | P: AsRef, 99 | Error: From, 100 | { 101 | match std::fs::read_to_string(&config_path) { 102 | Ok(s) => Ok(toml::from_str::(s.as_str()).unwrap()), 103 | Err(e) if e.kind() == ErrorKind::NotFound => { 104 | let config = Config::default(); 105 | std::fs::write(config_path, toml::to_string(&config).unwrap())?; 106 | Ok(config) 107 | } 108 | Err(e) => { 109 | error!("{:?}", e); 110 | panic!(); 111 | } 112 | } 113 | } 114 | 115 | /// Scan sysfs for sensor files 116 | pub fn load_temp_inputs(hw_mon: &HwMon) -> Vec { 117 | let dir = match std::fs::read_dir(hw_mon.mon_dir()) { 118 | Ok(d) => d, 119 | _ => return vec![], 120 | }; 121 | dir.filter_map(|f| f.ok()) 122 | .filter_map(|f| { 123 | f.file_name() 124 | .to_str() 125 | .filter(|s| s.starts_with("temp") && s.ends_with("_input")) 126 | .map(String::from) 127 | }) 128 | .collect() 129 | } 130 | 131 | /// Create config directory if does not exists 132 | pub fn ensure_config_dir() -> Result<()> { 133 | if std::fs::read(CONFIG_DIR).map_err(|e| e.kind() == ErrorKind::NotFound) == Err(true) { 134 | std::fs::create_dir_all(CONFIG_DIR).map_err(|e| AmdGpuError::CreatePath { 135 | io: e, 136 | path: CONFIG_DIR.into(), 137 | })?; 138 | } 139 | Ok(()) 140 | } 141 | -------------------------------------------------------------------------------- /crates/amdgui-helper/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdgui-helper/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdgui-helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdgui-helper" 3 | version = "1.0.14" 4 | edition = "2018" 5 | description = "AMDGPU fan control service" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | target = "x86_64-unknown-linux-musl" 15 | 16 | [[bin]] 17 | name = "amdgui-helper" 18 | path = "./src/main.rs" 19 | 20 | [features] 21 | static = ["eyra"] 22 | 23 | [dependencies] 24 | amdgpu = { path = "../amdgpu", version = "1.0.9", features = ["gui-helper"] } 25 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.9", features = ["fan", "gui"] } 26 | amdmond-lib = { path = "../amdmond-lib", version = "1.0.9" } 27 | eyra = { workspace = true, optional = true } 28 | gumdrop = { workspace = true } 29 | nix = { workspace = true } 30 | ron = { workspace = true } 31 | serde = { workspace = true, features = ["derive"] } 32 | sudo = { workspace = true } 33 | thiserror = { workspace = true } 34 | toml = { workspace = true } 35 | tracing = { workspace = true } 36 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 37 | 38 | [dev-dependencies] 39 | amdgpu = { path = "../amdgpu", version = "1.0" } 40 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] } 41 | amdmond-lib = { path = "../amdmond-lib", version = "1.0" } 42 | -------------------------------------------------------------------------------- /crates/amdgui-helper/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdgui-helper/README.md: -------------------------------------------------------------------------------- 1 | # amdgui-helper 2 | 3 | Daemon with elevated privileges to scan for `amdfand` daemons, reload them and save config files 4 | 5 | You can communicate with it using sock file `/tmp/amdgui-helper.sock` using helper `Command` from `amdgpu`. 6 | 7 | Each connection is single use and will be terminated after sending `Response`. 8 | -------------------------------------------------------------------------------- /crates/amdgui-helper/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "static")] 3 | println!("cargo:rustc-link-arg=-nostartfiles"); 4 | } 5 | -------------------------------------------------------------------------------- /crates/amdgui-helper/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Special daemon with root privileges. Since GUI should not have (and 2 | //! sometimes can't have) root privileges and service processes are designed to 3 | //! be as small as possible this is proxy. 4 | //! 5 | //! It is responsible for: 6 | //! * Loading all amdfand processes. In order to do this process needs to be 7 | //! killed with signal 0 to check if it still is alive 8 | //! * Reload amdfand process with signal SIGHUP 9 | //! * Save changed config file 10 | //! 11 | //! It is using `/tmp/amdgui-helper.sock` file and `ron` serialization for 12 | //! communication. After each operation connection is terminated so each command 13 | //! needs new connection. 14 | #![allow(clippy::non_octal_unix_permissions)] 15 | 16 | #[cfg(feature = "static")] 17 | extern crate eyra; 18 | 19 | use std::ffi::OsStr; 20 | use std::fs::Permissions; 21 | use std::os::unix::fs::PermissionsExt; 22 | use std::os::unix::net::UnixListener; 23 | 24 | use amdgpu::pidfile::helper_cmd::{Command, Response}; 25 | use amdgpu::pidfile::{handle_connection, Pid}; 26 | use amdgpu::IoFailure; 27 | use tracing::{error, info, warn}; 28 | 29 | #[derive(Debug, thiserror::Error)] 30 | pub enum Error { 31 | #[error("{0}")] 32 | Io(#[from] amdgpu::IoFailure), 33 | #[error("{0}")] 34 | Lock(#[from] amdgpu::AmdGpuError), 35 | } 36 | 37 | pub type Result = std::result::Result; 38 | 39 | fn main() -> Result<()> { 40 | if std::env::var("RUST_LOG").is_err() { 41 | std::env::set_var("RUST_LOG", "DEBUG"); 42 | } 43 | tracing_subscriber::fmt::init(); 44 | 45 | let mut lock = amdgpu::lock_file::PidLock::new("amdgui", String::from("helper"))?; 46 | lock.acquire()?; 47 | 48 | let sock_path = amdgpu::pidfile::helper_cmd::sock_file(); 49 | let listener = { 50 | let _ = std::fs::remove_file(&sock_path); 51 | 52 | UnixListener::bind(&sock_path).map_err(|io| IoFailure { 53 | io, 54 | path: sock_path.clone(), 55 | })? 56 | }; 57 | if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) { 58 | error!("Failed to change gui helper socket file mode. {:?}", e); 59 | } 60 | 61 | while let Ok((stream, _addr)) = listener.accept() { 62 | handle_connection::<_, Command, Response>(stream, handle_command); 63 | } 64 | 65 | lock.release()?; 66 | Ok(()) 67 | } 68 | 69 | pub type Service = amdgpu::pidfile::Service; 70 | 71 | fn handle_command(service: Service, cmd: Command) { 72 | match cmd { 73 | Command::ReloadConfig { pid } => { 74 | info!("Reloading config file for pid {:?}", pid); 75 | handle_reload_config(service, pid); 76 | } 77 | Command::FanServices => handle_fan_services(service), 78 | Command::SaveFanConfig { path, content } => handle_save_fan_config(service, path, content), 79 | } 80 | } 81 | 82 | fn handle_save_fan_config(mut service: Service, path: String, content: String) { 83 | match std::fs::write(path, content) { 84 | Err(e) => service.write_response(Response::ConfigFileSaveFailed(format!("{:?}", e))), 85 | Ok(..) => service.write_response(Response::ConfigFileSaved), 86 | } 87 | } 88 | 89 | fn handle_fan_services(mut service: Service) { 90 | info!("Loading fan services"); 91 | let services = read_fan_services(); 92 | info!("Loaded fan services pid {:?}", services); 93 | service.write_response(Response::Services(services)); 94 | } 95 | 96 | fn handle_reload_config(service: Service, pid: Pid) { 97 | unsafe { 98 | nix::libc::kill(pid.0, nix::sys::signal::Signal::SIGHUP as i32); 99 | } 100 | service.kill(); 101 | } 102 | 103 | fn read_fan_services() -> Vec { 104 | if let Ok(entry) = std::fs::read_dir("/var/lib/amdfand") { 105 | entry 106 | .filter(|e| { 107 | e.as_ref() 108 | .map(|e| { 109 | info!("Extension is {:?}", e.path().extension()); 110 | e.path().extension().and_then(OsStr::to_str) == Some("pid") 111 | }) 112 | .ok() 113 | .unwrap_or_default() 114 | }) 115 | .filter_map(|e| { 116 | info!("Found entry {:?}", e); 117 | match e { 118 | Ok(entry) => std::fs::read_to_string(entry.path()) 119 | .ok() 120 | .and_then(|s| s.parse::().ok()) 121 | .filter(|pid| unsafe { nix::libc::kill(*pid, 0) } == 0), 122 | _ => None, 123 | } 124 | }) 125 | .map(Pid) 126 | .collect() 127 | } else { 128 | warn!("Directory /var/lib/amdfand not found"); 129 | vec![] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /crates/amdgui/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-gnu" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdgui/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-gnu" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdgui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdguid" 3 | version = "1.0.14" 4 | edition = "2018" 5 | description = "AMDGPU fan control service" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | 11 | [dependencies] 12 | amdgpu = { path = "../amdgpu", version = "1", features = ["gui-helper"] } 13 | amdgpu-config = { path = "../amdgpu-config", version = "1", features = ["fan", "gui"] } 14 | amdmond-lib = { path = "../amdmond-lib", version = "1" } 15 | bytemuck = { workspace = true } 16 | eframe = { workspace = true } 17 | winit = { workspace = true } 18 | egui = { workspace = true } 19 | egui_plot = { workspace = true } 20 | epaint = { workspace = true } 21 | gumdrop = { workspace = true } 22 | image = { workspace = true, features = ["png"] } 23 | nix = { workspace = true } 24 | parking_lot = { workspace = true } 25 | serde = { workspace = true, features = ["derive"] } 26 | tokio = { workspace = true, features = ["full"] } 27 | toml = { workspace = true } 28 | tracing = { workspace = true } 29 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 30 | 31 | [dev-dependencies] 32 | amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] } 33 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] } 34 | amdmond-lib = { path = "../amdmond-lib", version = "1.0" } 35 | -------------------------------------------------------------------------------- /crates/amdgui/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdgui/README.md: -------------------------------------------------------------------------------- 1 | # AMD GPU gui tool 2 | 3 | Provides basic FAN configuration. 4 | 5 | ## Roadmap 6 | 7 | * amdvold config manipulation 8 | * Fix Drag & drop functionality - mouse is not followed properly 9 | * Program profiles 10 | 11 | ## Screenshots 12 | 13 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png) 14 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png) 15 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png) 16 | -------------------------------------------------------------------------------- /crates/amdgui/assets/icons/html_port-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdgui/assets/icons/html_port-512.png -------------------------------------------------------------------------------- /crates/amdgui/assets/icons/ports.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdgui/assets/icons/ports.jpg -------------------------------------------------------------------------------- /crates/amdgui/assets/icons/ports2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdgui/assets/icons/ports2.jpg -------------------------------------------------------------------------------- /crates/amdgui/assets/icons/ports2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdgui/assets/icons/ports2.png -------------------------------------------------------------------------------- /crates/amdgui/assets/icons/vga_port-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdgui/assets/icons/vga_port-512.png -------------------------------------------------------------------------------- /crates/amdgui/src/backend.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use egui::panel::TopBottomSide; 4 | use egui::{Align, Layout, PointerButton}; 5 | use parking_lot::Mutex; 6 | 7 | use crate::app::Page; 8 | use crate::AmdGui; 9 | 10 | pub fn create_ui(amd_gui: Arc>, ctx: &egui::Context) { 11 | egui::containers::TopBottomPanel::new(TopBottomSide::Top, "menu").show(ctx, |ui| { 12 | let mut child = ui.child_ui( 13 | ui.available_rect_before_wrap(), 14 | Layout::left_to_right(Align::default()), 15 | None, 16 | ); 17 | 18 | if child 19 | .add( 20 | egui::Button::new("Temp Config"), /* .text_style(TextStyle::Heading) */ 21 | ) 22 | .clicked_by(PointerButton::Primary) 23 | { 24 | amd_gui.lock().page = Page::TempConfig; 25 | } 26 | if child 27 | .add( 28 | egui::Button::new("Usage Config"), /* .text_style(TextStyle::Heading) */ 29 | ) 30 | .clicked_by(PointerButton::Primary) 31 | { 32 | amd_gui.lock().page = Page::UsageConfig; 33 | } 34 | if child 35 | .add( 36 | egui::Button::new("Monitoring"), /* .text_style(TextStyle::Heading) */ 37 | ) 38 | .clicked_by(PointerButton::Primary) 39 | { 40 | amd_gui.lock().page = Page::Monitoring; 41 | } 42 | if child 43 | .add( 44 | egui::Button::new("Outputs"), /* .text_style(TextStyle::Heading) */ 45 | ) 46 | .clicked_by(PointerButton::Primary) 47 | { 48 | amd_gui.lock().page = Page::Outputs; 49 | } 50 | if child 51 | .add( 52 | egui::Button::new("Settings"), /* .text_style(TextStyle::Heading) */ 53 | ) 54 | .clicked_by(PointerButton::Primary) 55 | { 56 | amd_gui.lock().page = Page::Settings; 57 | } 58 | }); 59 | 60 | egui::containers::CentralPanel::default().show(ctx, |ui| { 61 | let mut gui = amd_gui.lock(); 62 | match gui.page { 63 | Page::TempConfig => { 64 | gui.ui(ui); 65 | } 66 | Page::UsageConfig => { 67 | gui.ui(ui); 68 | } 69 | Page::Monitoring => { 70 | gui.ui(ui); 71 | } 72 | Page::Outputs => { 73 | gui.ui(ui); 74 | } 75 | Page::Settings => { 76 | ctx.settings_ui(ui); 77 | } 78 | } 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /crates/amdgui/src/items.rs: -------------------------------------------------------------------------------- 1 | //! Contains items that can be added to a plot. 2 | 3 | use std::ops::RangeInclusive; 4 | 5 | pub use arrows::*; 6 | use egui::{Color32, Pos2, Shape, Stroke}; 7 | pub use h_line::*; 8 | pub use line::*; 9 | pub use marker_shape::*; 10 | pub use plot_image::*; 11 | pub use plot_item::*; 12 | pub use points::*; 13 | pub use polygons::*; 14 | pub use text::*; 15 | pub use v_line::*; 16 | pub use value::Value; 17 | pub use values::Values; 18 | 19 | mod arrows; 20 | mod h_line; 21 | mod line; 22 | mod marker_shape; 23 | mod plot_image; 24 | mod plot_item; 25 | mod points; 26 | mod polygons; 27 | mod text; 28 | mod v_line; 29 | mod value; 30 | mod values; 31 | 32 | const DEFAULT_FILL_ALPHA: f32 = 0.05; 33 | 34 | // ---------------------------------------------------------------------------- 35 | 36 | #[derive(Clone, Copy, Debug, PartialEq)] 37 | pub enum LineStyle { 38 | Solid, 39 | Dotted { spacing: f32 }, 40 | Dashed { length: f32 }, 41 | } 42 | 43 | impl LineStyle { 44 | pub fn dashed_loose() -> Self { 45 | Self::Dashed { length: 10.0 } 46 | } 47 | 48 | pub fn dashed_dense() -> Self { 49 | Self::Dashed { length: 5.0 } 50 | } 51 | 52 | pub fn dotted_loose() -> Self { 53 | Self::Dotted { spacing: 10.0 } 54 | } 55 | 56 | pub fn dotted_dense() -> Self { 57 | Self::Dotted { spacing: 5.0 } 58 | } 59 | 60 | fn style_line( 61 | &self, 62 | line: Vec, 63 | mut stroke: Stroke, 64 | highlight: bool, 65 | shapes: &mut Vec, 66 | ) { 67 | match line.len() { 68 | 0 => {} 69 | 1 => { 70 | let mut radius = stroke.width / 2.0; 71 | if highlight { 72 | radius *= 2f32.sqrt(); 73 | } 74 | shapes.push(Shape::circle_filled(line[0], radius, stroke.color)); 75 | } 76 | _ => { 77 | match self { 78 | LineStyle::Solid => { 79 | if highlight { 80 | stroke.width *= 2.0; 81 | } 82 | for point in line.iter() { 83 | shapes.push(Shape::circle_filled( 84 | *point, 85 | stroke.width * 3.0, 86 | Color32::DARK_BLUE, 87 | )); 88 | } 89 | shapes.push(Shape::line(line, stroke)); 90 | } 91 | LineStyle::Dotted { spacing } => { 92 | let mut radius = stroke.width; 93 | if highlight { 94 | radius *= 2f32.sqrt(); 95 | } 96 | shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius)); 97 | } 98 | LineStyle::Dashed { length } => { 99 | if highlight { 100 | stroke.width *= 2.0; 101 | } 102 | let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 103 | shapes.extend(Shape::dashed_line( 104 | &line, 105 | stroke, 106 | *length, 107 | length * golden_ratio, 108 | )); 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | impl ToString for LineStyle { 117 | fn to_string(&self) -> String { 118 | match self { 119 | LineStyle::Solid => "Solid".into(), 120 | LineStyle::Dotted { spacing } => format!("Dotted{}Px", spacing), 121 | LineStyle::Dashed { length } => format!("Dashed{}Px", length), 122 | } 123 | } 124 | } 125 | 126 | // ---------------------------------------------------------------------------- 127 | 128 | // ---------------------------------------------------------------------------- 129 | 130 | /// Describes a function y = f(x) with an optional range for x and a number of 131 | /// points. 132 | pub struct ExplicitGenerator { 133 | function: Box f64>, 134 | x_range: RangeInclusive, 135 | points: usize, 136 | } 137 | 138 | // ---------------------------------------------------------------------------- 139 | 140 | /// Returns the x-coordinate of a possible intersection between a line segment 141 | /// from `p1` to `p2` and a horizontal line at the given y-coordinate. 142 | #[inline(always)] 143 | pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { 144 | ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y)) 145 | .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) 146 | } 147 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/arrows.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Color32, Shape, Stroke, Ui}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::values::Values; 7 | use crate::transform::{Bounds, ScreenTransform}; 8 | 9 | /// A set of arrows. 10 | pub struct Arrows { 11 | pub(crate) origins: Values, 12 | pub(crate) tips: Values, 13 | pub(crate) color: Color32, 14 | pub(crate) name: String, 15 | pub(crate) highlight: bool, 16 | } 17 | 18 | impl Arrows { 19 | pub fn new(origins: Values, tips: Values) -> Self { 20 | Self { 21 | origins, 22 | tips, 23 | color: Color32::TRANSPARENT, 24 | name: Default::default(), 25 | highlight: false, 26 | } 27 | } 28 | } 29 | 30 | impl PlotItem for Arrows { 31 | fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 32 | use egui::emath::*; 33 | let Self { 34 | origins, 35 | tips, 36 | color, 37 | highlight, 38 | .. 39 | } = self; 40 | let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color); 41 | origins 42 | .values 43 | .iter() 44 | .zip(tips.values.iter()) 45 | .map(|(origin, tip)| { 46 | ( 47 | transform.position_from_value(origin), 48 | transform.position_from_value(tip), 49 | ) 50 | }) 51 | .for_each(|(origin, tip)| { 52 | let vector = tip - origin; 53 | let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); 54 | let tip_length = vector.length() / 4.0; 55 | let tip = origin + vector; 56 | let dir = vector.normalized(); 57 | shapes.push(Shape::line_segment([origin, tip], stroke)); 58 | shapes.push(Shape::line( 59 | vec![ 60 | tip - tip_length * (rot.inverse() * dir), 61 | tip, 62 | tip - tip_length * (rot * dir), 63 | ], 64 | stroke, 65 | )); 66 | }); 67 | } 68 | 69 | fn initialize(&mut self, _x_range: RangeInclusive) { 70 | self.origins 71 | .generate_points(f64::NEG_INFINITY..=f64::INFINITY); 72 | self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY); 73 | } 74 | 75 | fn name(&self) -> &str { 76 | self.name.as_str() 77 | } 78 | 79 | fn color(&self) -> Color32 { 80 | self.color 81 | } 82 | 83 | fn highlight(&mut self) { 84 | self.highlight = true; 85 | } 86 | 87 | fn highlighted(&self) -> bool { 88 | self.highlight 89 | } 90 | 91 | fn values(&self) -> Option<&Values> { 92 | Some(&self.origins) 93 | } 94 | 95 | fn bounds(&self) -> Bounds { 96 | self.origins.get_bounds() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/h_line.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Color32, Shape, Stroke, Ui}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::value::Value; 7 | use crate::items::values::Values; 8 | use crate::items::LineStyle; 9 | use crate::transform::{Bounds, ScreenTransform}; 10 | 11 | /// A horizontal line in a plot, filling the full width 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct HLine { 14 | pub(crate) y: f64, 15 | pub(crate) stroke: Stroke, 16 | pub(crate) name: String, 17 | pub(crate) highlight: bool, 18 | pub(crate) style: LineStyle, 19 | } 20 | 21 | impl HLine { 22 | pub fn new(y: impl Into) -> Self { 23 | Self { 24 | y: y.into(), 25 | stroke: Stroke::new(1.0, Color32::TRANSPARENT), 26 | name: String::default(), 27 | highlight: false, 28 | style: LineStyle::Solid, 29 | } 30 | } 31 | 32 | /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will 33 | /// be auto-assigned. 34 | #[must_use] 35 | pub fn color(mut self, color: impl Into) -> Self { 36 | self.stroke.color = color.into(); 37 | self 38 | } 39 | } 40 | 41 | impl PlotItem for HLine { 42 | fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 43 | let HLine { 44 | y, 45 | stroke, 46 | highlight, 47 | style, 48 | .. 49 | } = self; 50 | let points = vec![ 51 | transform.position_from_value(&Value::new(transform.bounds().min[0], *y)), 52 | transform.position_from_value(&Value::new(transform.bounds().max[0], *y)), 53 | ]; 54 | style.style_line(points, *stroke, *highlight, shapes); 55 | } 56 | 57 | fn initialize(&mut self, _x_range: RangeInclusive) {} 58 | 59 | fn name(&self) -> &str { 60 | &self.name 61 | } 62 | 63 | fn color(&self) -> Color32 { 64 | self.stroke.color 65 | } 66 | 67 | fn highlight(&mut self) { 68 | self.highlight = true; 69 | } 70 | 71 | fn highlighted(&self) -> bool { 72 | self.highlight 73 | } 74 | 75 | fn values(&self) -> Option<&Values> { 76 | None 77 | } 78 | 79 | fn bounds(&self) -> Bounds { 80 | let mut bounds = Bounds::NOTHING; 81 | bounds.min[1] = self.y; 82 | bounds.max[1] = self.y; 83 | bounds 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/line.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | use std::sync::Arc; 3 | 4 | use egui::{pos2, Color32, Mesh, NumExt, Rgba, Shape, Stroke, Ui}; 5 | 6 | use crate::items; 7 | use crate::items::plot_item::PlotItem; 8 | use crate::items::value::Value; 9 | use crate::items::values::Values; 10 | use crate::items::{LineStyle, DEFAULT_FILL_ALPHA}; 11 | use crate::transform::{Bounds, ScreenTransform}; 12 | 13 | impl PlotItem for Line { 14 | fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 15 | let Self { 16 | series, 17 | stroke, 18 | highlight, 19 | mut fill, 20 | style, 21 | .. 22 | } = self; 23 | 24 | let values_tf: Vec<_> = series 25 | .values 26 | .iter() 27 | .map(|v| transform.position_from_value(v)) 28 | .collect(); 29 | let n_values = values_tf.len(); 30 | 31 | // Fill the area between the line and a reference line, if required. 32 | if n_values < 2 { 33 | fill = None; 34 | } 35 | if let Some(y_reference) = fill { 36 | let mut fill_alpha = DEFAULT_FILL_ALPHA; 37 | if *highlight { 38 | fill_alpha = (2.0 * fill_alpha).at_most(1.0); 39 | } 40 | let y = transform 41 | .position_from_value(&Value::new(0.0, y_reference)) 42 | .y; 43 | let fill_color = Rgba::from(stroke.color) 44 | .to_opaque() 45 | .multiply(fill_alpha) 46 | .into(); 47 | let mut mesh = Mesh::default(); 48 | let expected_intersections = 20; 49 | mesh.reserve_triangles((n_values - 1) * 2); 50 | mesh.reserve_vertices(n_values * 2 + expected_intersections); 51 | values_tf[0..n_values - 1].windows(2).for_each(|w| { 52 | let i = mesh.vertices.len() as u32; 53 | mesh.colored_vertex(w[0], fill_color); 54 | mesh.colored_vertex(pos2(w[0].x, y), fill_color); 55 | if let Some(x) = items::y_intersection(&w[0], &w[1], y) { 56 | let point = pos2(x, y); 57 | mesh.colored_vertex(point, fill_color); 58 | mesh.add_triangle(i, i + 1, i + 2); 59 | mesh.add_triangle(i + 2, i + 3, i + 4); 60 | } else { 61 | mesh.add_triangle(i, i + 1, i + 2); 62 | mesh.add_triangle(i + 1, i + 2, i + 3); 63 | } 64 | }); 65 | let last = values_tf[n_values - 1]; 66 | mesh.colored_vertex(last, fill_color); 67 | mesh.colored_vertex(pos2(last.x, y), fill_color); 68 | shapes.push(Shape::Mesh(Arc::new(mesh))); 69 | } 70 | 71 | style.style_line(values_tf, *stroke, *highlight, shapes); 72 | } 73 | 74 | fn initialize(&mut self, x_range: RangeInclusive) { 75 | self.series.generate_points(x_range); 76 | } 77 | 78 | fn name(&self) -> &str { 79 | self.name.as_str() 80 | } 81 | 82 | fn color(&self) -> Color32 { 83 | self.stroke.color 84 | } 85 | 86 | fn highlight(&mut self) { 87 | self.highlight = true; 88 | } 89 | 90 | fn highlighted(&self) -> bool { 91 | self.highlight 92 | } 93 | 94 | fn values(&self) -> Option<&Values> { 95 | Some(&self.series) 96 | } 97 | 98 | fn bounds(&self) -> Bounds { 99 | self.series.get_bounds() 100 | } 101 | } 102 | 103 | /// A series of values forming a path. 104 | pub struct Line { 105 | pub series: Values, 106 | pub stroke: Stroke, 107 | pub name: String, 108 | pub highlight: bool, 109 | pub fill: Option, 110 | pub style: LineStyle, 111 | } 112 | 113 | impl Line { 114 | pub fn new(series: Values) -> Self { 115 | Self { 116 | series, 117 | stroke: Stroke::new(1.0, Color32::TRANSPARENT), 118 | name: Default::default(), 119 | highlight: false, 120 | fill: None, 121 | style: LineStyle::Solid, 122 | } 123 | } 124 | 125 | /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will 126 | /// be auto-assigned. 127 | #[must_use] 128 | pub fn color(mut self, color: impl Into) -> Self { 129 | self.stroke.color = color.into(); 130 | self 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/marker_shape.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 2 | pub enum MarkerShape { 3 | Circle, 4 | Diamond, 5 | Square, 6 | Cross, 7 | Plus, 8 | Up, 9 | Down, 10 | Left, 11 | Right, 12 | Asterisk, 13 | } 14 | 15 | impl MarkerShape { 16 | /// Get a vector containing all marker shapes. 17 | pub fn all() -> impl Iterator { 18 | [ 19 | Self::Circle, 20 | Self::Diamond, 21 | Self::Square, 22 | Self::Cross, 23 | Self::Plus, 24 | Self::Up, 25 | Self::Down, 26 | Self::Left, 27 | Self::Right, 28 | Self::Asterisk, 29 | ] 30 | .iter() 31 | .copied() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/plot_image.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{pos2, Color32, Image, Rect, Shape, Stroke, StrokeKind, TextureId, Ui, Vec2}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::value::Value; 7 | use crate::items::values::Values; 8 | use crate::transform::{Bounds, ScreenTransform}; 9 | 10 | /// An image in the plot. 11 | pub struct PlotImage { 12 | pub position: Value, 13 | pub texture_id: TextureId, 14 | pub uv: Rect, 15 | pub size: Vec2, 16 | pub bg_fill: Color32, 17 | pub tint: Color32, 18 | pub highlight: bool, 19 | pub name: String, 20 | } 21 | 22 | impl PlotImage { 23 | /// Create a new image with position and size in plot coordinates. 24 | pub fn new(texture_id: TextureId, position: Value, size: impl Into) -> Self { 25 | Self { 26 | position, 27 | name: Default::default(), 28 | highlight: false, 29 | texture_id, 30 | uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), 31 | size: size.into(), 32 | bg_fill: Default::default(), 33 | tint: Color32::WHITE, 34 | } 35 | } 36 | 37 | /// Highlight this image in the plot. 38 | #[must_use] 39 | pub fn highlight(mut self) -> Self { 40 | self.highlight = true; 41 | self 42 | } 43 | 44 | /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. 45 | #[must_use] 46 | pub fn uv(mut self, uv: impl Into) -> Self { 47 | self.uv = uv.into(); 48 | self 49 | } 50 | 51 | /// A solid color to put behind the image. Useful for transparent images. 52 | #[must_use] 53 | pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { 54 | self.bg_fill = bg_fill.into(); 55 | self 56 | } 57 | 58 | /// Multiply image color with this. Default is WHITE (no tint). 59 | #[must_use] 60 | pub fn tint(mut self, tint: impl Into) -> Self { 61 | self.tint = tint.into(); 62 | self 63 | } 64 | 65 | /// Name of this image. 66 | /// 67 | /// This name will show up in the plot legend, if legends are turned on. 68 | /// 69 | /// Multiple plot items may share the same name, in which case they will 70 | /// also share an entry in the legend. 71 | #[allow(clippy::needless_pass_by_value)] 72 | #[must_use] 73 | pub fn name(mut self, name: impl ToString) -> Self { 74 | self.name = name.to_string(); 75 | self 76 | } 77 | } 78 | 79 | impl PlotItem for PlotImage { 80 | fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 81 | let Self { 82 | position, 83 | texture_id, 84 | uv, 85 | size, 86 | bg_fill, 87 | tint, 88 | highlight, 89 | .. 90 | } = self; 91 | let rect = { 92 | let left_top = Value::new( 93 | position.x as f32 - size.x / 2.0, 94 | position.y as f32 - size.y / 2.0, 95 | ); 96 | let right_bottom = Value::new( 97 | position.x as f32 + size.x / 2.0, 98 | position.y as f32 + size.y / 2.0, 99 | ); 100 | let left_top_tf = transform.position_from_value(&left_top); 101 | let right_bottom_tf = transform.position_from_value(&right_bottom); 102 | Rect::from_two_pos(left_top_tf, right_bottom_tf) 103 | }; 104 | Image::new((*texture_id, *size)) 105 | .bg_fill(*bg_fill) 106 | .tint(*tint) 107 | .uv(*uv) 108 | .paint_at(ui, rect); 109 | if *highlight { 110 | shapes.push(Shape::rect_stroke( 111 | rect, 112 | 0.0, 113 | Stroke::new(1.0, ui.visuals().strong_text_color()), 114 | StrokeKind::Inside, 115 | )); 116 | } 117 | } 118 | 119 | fn initialize(&mut self, _x_range: RangeInclusive) {} 120 | 121 | fn name(&self) -> &str { 122 | self.name.as_str() 123 | } 124 | 125 | fn color(&self) -> Color32 { 126 | Color32::TRANSPARENT 127 | } 128 | 129 | fn highlight(&mut self) { 130 | self.highlight = true; 131 | } 132 | 133 | fn highlighted(&self) -> bool { 134 | self.highlight 135 | } 136 | 137 | fn values(&self) -> Option<&Values> { 138 | None 139 | } 140 | 141 | fn bounds(&self) -> Bounds { 142 | let mut bounds = Bounds::NOTHING; 143 | let left_top = Value::new( 144 | self.position.x as f32 - self.size.x / 2.0, 145 | self.position.y as f32 - self.size.y / 2.0, 146 | ); 147 | let right_bottom = Value::new( 148 | self.position.x as f32 + self.size.x / 2.0, 149 | self.position.y as f32 + self.size.y / 2.0, 150 | ); 151 | bounds.extend_with(&left_top); 152 | bounds.extend_with(&right_bottom); 153 | bounds 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/plot_item.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Color32, Shape, Ui}; 4 | 5 | use crate::items::Values; 6 | use crate::transform::{Bounds, ScreenTransform}; 7 | 8 | /// Trait shared by things that can be drawn in the plot. 9 | pub trait PlotItem { 10 | fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec); 11 | fn initialize(&mut self, x_range: RangeInclusive); 12 | fn name(&self) -> &str; 13 | fn color(&self) -> Color32; 14 | fn highlight(&mut self); 15 | fn highlighted(&self) -> bool; 16 | fn values(&self) -> Option<&Values>; 17 | fn bounds(&self) -> Bounds; 18 | } 19 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/polygons.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Color32, NumExt, Rgba, Shape, Stroke, Ui}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::values::Values; 7 | use crate::items::{LineStyle, DEFAULT_FILL_ALPHA}; 8 | use crate::transform::{Bounds, ScreenTransform}; 9 | 10 | /// A convex polygon. 11 | pub struct Polygon { 12 | pub series: Values, 13 | pub stroke: Stroke, 14 | pub name: String, 15 | pub highlight: bool, 16 | pub fill_alpha: f32, 17 | pub style: LineStyle, 18 | } 19 | 20 | impl Polygon { 21 | pub fn new(series: Values) -> Self { 22 | Self { 23 | series, 24 | stroke: Stroke::new(1.0, Color32::TRANSPARENT), 25 | name: Default::default(), 26 | highlight: false, 27 | fill_alpha: DEFAULT_FILL_ALPHA, 28 | style: LineStyle::Solid, 29 | } 30 | } 31 | 32 | /// Highlight this polygon in the plot by scaling up the stroke and reducing 33 | /// the fill transparency. 34 | #[must_use] 35 | pub fn highlight(mut self) -> Self { 36 | self.highlight = true; 37 | self 38 | } 39 | 40 | /// Add a custom stroke. 41 | #[must_use] 42 | pub fn stroke(mut self, stroke: impl Into) -> Self { 43 | self.stroke = stroke.into(); 44 | self 45 | } 46 | 47 | /// Set the stroke width. 48 | #[must_use] 49 | pub fn width(mut self, width: impl Into) -> Self { 50 | self.stroke.width = width.into(); 51 | self 52 | } 53 | 54 | /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will 55 | /// be auto-assigned. 56 | #[must_use] 57 | pub fn color(mut self, color: impl Into) -> Self { 58 | self.stroke.color = color.into(); 59 | self 60 | } 61 | 62 | /// Alpha of the filled area. 63 | #[must_use] 64 | pub fn fill_alpha(mut self, alpha: impl Into) -> Self { 65 | self.fill_alpha = alpha.into(); 66 | self 67 | } 68 | 69 | /// Set the outline's style. Default is `LineStyle::Solid`. 70 | #[must_use] 71 | pub fn style(mut self, style: LineStyle) -> Self { 72 | self.style = style; 73 | self 74 | } 75 | 76 | /// Name of this polygon. 77 | /// 78 | /// This name will show up in the plot legend, if legends are turned on. 79 | /// 80 | /// Multiple plot items may share the same name, in which case they will 81 | /// also share an entry in the legend. 82 | #[allow(clippy::needless_pass_by_value)] 83 | #[must_use] 84 | pub fn name(mut self, name: impl ToString) -> Self { 85 | self.name = name.to_string(); 86 | self 87 | } 88 | } 89 | 90 | impl PlotItem for Polygon { 91 | fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 92 | let Self { 93 | series, 94 | stroke, 95 | highlight, 96 | mut fill_alpha, 97 | style, 98 | .. 99 | } = self; 100 | 101 | if *highlight { 102 | fill_alpha = (2.0 * fill_alpha).at_most(1.0); 103 | } 104 | 105 | let mut values_tf: Vec<_> = series 106 | .values 107 | .iter() 108 | .map(|v| transform.position_from_value(v)) 109 | .collect(); 110 | 111 | let fill = Rgba::from(stroke.color).to_opaque().multiply(fill_alpha); 112 | 113 | let shape = Shape::convex_polygon(values_tf.clone(), fill, Stroke::NONE); 114 | shapes.push(shape); 115 | values_tf.push(*values_tf.first().unwrap()); 116 | style.style_line(values_tf, *stroke, *highlight, shapes); 117 | } 118 | 119 | fn initialize(&mut self, x_range: RangeInclusive) { 120 | self.series.generate_points(x_range); 121 | } 122 | 123 | fn name(&self) -> &str { 124 | self.name.as_str() 125 | } 126 | 127 | fn color(&self) -> Color32 { 128 | self.stroke.color 129 | } 130 | 131 | fn highlight(&mut self) { 132 | self.highlight = true; 133 | } 134 | 135 | fn highlighted(&self) -> bool { 136 | self.highlight 137 | } 138 | 139 | fn values(&self) -> Option<&Values> { 140 | Some(&self.series) 141 | } 142 | 143 | fn bounds(&self) -> Bounds { 144 | self.series.get_bounds() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/text.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Align2, Color32, Rect, Shape, Stroke, StrokeKind, TextStyle, Ui}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::value::Value; 7 | use crate::items::values::Values; 8 | use crate::transform::{Bounds, ScreenTransform}; 9 | 10 | /// Text inside the plot. 11 | pub struct Text { 12 | pub(crate) text: String, 13 | pub(crate) style: TextStyle, 14 | pub(crate) position: Value, 15 | pub(crate) name: String, 16 | pub(crate) highlight: bool, 17 | pub(crate) color: Color32, 18 | pub(crate) anchor: Align2, 19 | } 20 | 21 | impl Text { 22 | #[allow(clippy::needless_pass_by_value)] 23 | pub fn new(position: Value, text: impl ToString) -> Self { 24 | Self { 25 | text: text.to_string(), 26 | style: TextStyle::Small, 27 | position, 28 | name: Default::default(), 29 | highlight: false, 30 | color: Color32::TRANSPARENT, 31 | anchor: Align2::CENTER_CENTER, 32 | } 33 | } 34 | 35 | /// Highlight this text in the plot by drawing a rectangle around it. 36 | #[must_use] 37 | pub fn highlight(mut self) -> Self { 38 | self.highlight = true; 39 | self 40 | } 41 | 42 | /// Text style. Default is `TextStyle::Small`. 43 | #[must_use] 44 | pub fn style(mut self, style: TextStyle) -> Self { 45 | self.style = style; 46 | self 47 | } 48 | 49 | /// Text color. Default is `Color32::TRANSPARENT` which means a color will 50 | /// be auto-assigned. 51 | #[must_use] 52 | pub fn color(mut self, color: impl Into) -> Self { 53 | self.color = color.into(); 54 | self 55 | } 56 | 57 | /// Anchor position of the text. Default is `Align2::CENTER_CENTER`. 58 | #[must_use] 59 | pub fn anchor(mut self, anchor: Align2) -> Self { 60 | self.anchor = anchor; 61 | self 62 | } 63 | 64 | /// Name of this text. 65 | /// 66 | /// This name will show up in the plot legend, if legends are turned on. 67 | /// 68 | /// Multiple plot items may share the same name, in which case they will 69 | /// also share an entry in the legend. 70 | #[allow(clippy::needless_pass_by_value)] 71 | #[must_use] 72 | pub fn name(mut self, name: impl ToString) -> Self { 73 | self.name = name.to_string(); 74 | self 75 | } 76 | } 77 | 78 | impl PlotItem for Text { 79 | fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 80 | let color = if self.color == Color32::TRANSPARENT { 81 | ui.style().visuals.text_color() 82 | } else { 83 | self.color 84 | }; 85 | let fond_id = ui.style().text_styles.get(&self.style).unwrap(); 86 | let pos = transform.position_from_value(&self.position); 87 | let galley = 88 | ui.fonts(|font| font.layout_no_wrap(self.text.clone(), fond_id.clone(), color)); 89 | let rect = self 90 | .anchor 91 | .anchor_rect(Rect::from_min_size(pos, galley.size())); 92 | shapes.push(Shape::galley(rect.min, galley, Color32::default())); 93 | if self.highlight { 94 | shapes.push(Shape::rect_stroke( 95 | rect.expand(2.0), 96 | 1.0, 97 | Stroke::new(0.5, color), 98 | StrokeKind::Inside, 99 | )); 100 | } 101 | } 102 | 103 | fn initialize(&mut self, _x_range: RangeInclusive) {} 104 | 105 | fn name(&self) -> &str { 106 | self.name.as_str() 107 | } 108 | 109 | fn color(&self) -> Color32 { 110 | self.color 111 | } 112 | 113 | fn highlight(&mut self) { 114 | self.highlight = true; 115 | } 116 | 117 | fn highlighted(&self) -> bool { 118 | self.highlight 119 | } 120 | 121 | fn values(&self) -> Option<&Values> { 122 | None 123 | } 124 | 125 | fn bounds(&self) -> Bounds { 126 | let mut bounds = Bounds::NOTHING; 127 | bounds.extend_with(&self.position); 128 | bounds 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/v_line.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use egui::{Color32, Shape, Stroke, Ui}; 4 | 5 | use crate::items::plot_item::PlotItem; 6 | use crate::items::value::Value; 7 | use crate::items::values::Values; 8 | use crate::items::LineStyle; 9 | use crate::transform::{Bounds, ScreenTransform}; 10 | 11 | impl PlotItem for VLine { 12 | fn shapes(&self, _ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec) { 13 | let VLine { 14 | x, 15 | stroke, 16 | highlight, 17 | style, 18 | .. 19 | } = self; 20 | let points = vec![ 21 | transform.position_from_value(&Value::new(*x, transform.bounds().min[1])), 22 | transform.position_from_value(&Value::new(*x, transform.bounds().max[1])), 23 | ]; 24 | style.style_line(points, *stroke, *highlight, shapes); 25 | } 26 | 27 | fn initialize(&mut self, _x_range: RangeInclusive) {} 28 | 29 | fn name(&self) -> &str { 30 | &self.name 31 | } 32 | 33 | fn color(&self) -> Color32 { 34 | self.stroke.color 35 | } 36 | 37 | fn highlight(&mut self) { 38 | self.highlight = true; 39 | } 40 | 41 | fn highlighted(&self) -> bool { 42 | self.highlight 43 | } 44 | 45 | fn values(&self) -> Option<&Values> { 46 | None 47 | } 48 | 49 | fn bounds(&self) -> Bounds { 50 | let mut bounds = Bounds::NOTHING; 51 | bounds.min[0] = self.x; 52 | bounds.max[0] = self.x; 53 | bounds 54 | } 55 | } 56 | 57 | /// A vertical line in a plot, filling the full width 58 | #[derive(Clone, Debug, PartialEq)] 59 | pub struct VLine { 60 | pub(crate) x: f64, 61 | pub(crate) stroke: Stroke, 62 | pub(crate) name: String, 63 | pub(crate) highlight: bool, 64 | pub(crate) style: LineStyle, 65 | } 66 | 67 | impl VLine { 68 | pub fn new(x: impl Into) -> Self { 69 | Self { 70 | x: x.into(), 71 | stroke: Stroke::new(1.0, Color32::TRANSPARENT), 72 | name: String::default(), 73 | highlight: false, 74 | style: LineStyle::Solid, 75 | } 76 | } 77 | 78 | /// Highlight this line in the plot by scaling up the line. 79 | #[must_use] 80 | pub fn highlight(mut self) -> Self { 81 | self.highlight = true; 82 | self 83 | } 84 | 85 | /// Add a stroke. 86 | #[must_use] 87 | pub fn stroke(mut self, stroke: impl Into) -> Self { 88 | self.stroke = stroke.into(); 89 | self 90 | } 91 | 92 | /// Stroke width. A high value means the plot thickens. 93 | #[must_use] 94 | pub fn width(mut self, width: impl Into) -> Self { 95 | self.stroke.width = width.into(); 96 | self 97 | } 98 | 99 | /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will 100 | /// be auto-assigned. 101 | #[must_use] 102 | pub fn color(mut self, color: impl Into) -> Self { 103 | self.stroke.color = color.into(); 104 | self 105 | } 106 | 107 | /// Set the line's style. Default is `LineStyle::Solid`. 108 | #[must_use] 109 | pub fn style(mut self, style: LineStyle) -> Self { 110 | self.style = style; 111 | self 112 | } 113 | 114 | /// Name of this vertical line. 115 | /// 116 | /// This name will show up in the plot legend, if legends are turned on. 117 | /// 118 | /// Multiple plot items may share the same name, in which case they will 119 | /// also share an entry in the legend. 120 | #[allow(clippy::needless_pass_by_value)] 121 | #[must_use] 122 | pub fn name(mut self, name: impl ToString) -> Self { 123 | self.name = name.to_string(); 124 | self 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/value.rs: -------------------------------------------------------------------------------- 1 | /// A value in the value-space of the plot. 2 | /// 3 | /// Uses f64 for improved accuracy to enable plotting 4 | /// large values (e.g. unix time on x axis). 5 | #[derive(Clone, Copy, Debug, PartialEq)] 6 | pub struct Value { 7 | /// This is often something monotonically increasing, such as time, but 8 | /// doesn't have to be. Goes from left to right. 9 | pub x: f64, 10 | /// Goes from bottom to top (inverse of everything else in egui!). 11 | pub y: f64, 12 | } 13 | 14 | impl Value { 15 | #[inline(always)] 16 | pub fn new(x: impl Into, y: impl Into) -> Self { 17 | Self { 18 | x: x.into(), 19 | y: y.into(), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/amdgui/src/items/values.rs: -------------------------------------------------------------------------------- 1 | use std::collections::Bound; 2 | use std::ops::{RangeBounds, RangeInclusive}; 3 | 4 | use crate::items::{ExplicitGenerator, Value}; 5 | use crate::transform::Bounds; 6 | 7 | pub struct Values { 8 | pub values: Vec, 9 | generator: Option, 10 | } 11 | 12 | impl Values { 13 | pub fn from_values(values: Vec) -> Self { 14 | Self { 15 | values, 16 | generator: None, 17 | } 18 | } 19 | 20 | pub fn from_values_iter(iter: impl Iterator) -> Self { 21 | Self::from_values(iter.collect()) 22 | } 23 | 24 | /// Draw a line based on a function `y=f(x)`, a range (which can be 25 | /// infinite) for x and the number of points. 26 | pub fn from_explicit_callback( 27 | function: impl Fn(f64) -> f64 + 'static, 28 | x_range: impl RangeBounds, 29 | points: usize, 30 | ) -> Self { 31 | let start = match x_range.start_bound() { 32 | Bound::Included(x) | Bound::Excluded(x) => *x, 33 | Bound::Unbounded => f64::NEG_INFINITY, 34 | }; 35 | let end = match x_range.end_bound() { 36 | Bound::Included(x) | Bound::Excluded(x) => *x, 37 | Bound::Unbounded => f64::INFINITY, 38 | }; 39 | let x_range = start..=end; 40 | 41 | let generator = ExplicitGenerator { 42 | function: Box::new(function), 43 | x_range, 44 | points, 45 | }; 46 | 47 | Self { 48 | values: Vec::new(), 49 | generator: Some(generator), 50 | } 51 | } 52 | 53 | /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the 54 | /// number of points. The range may be specified as start..end or as 55 | /// start..=end. 56 | pub fn from_parametric_callback( 57 | function: impl Fn(f64) -> (f64, f64), 58 | t_range: impl RangeBounds, 59 | points: usize, 60 | ) -> Self { 61 | let start = match t_range.start_bound() { 62 | Bound::Included(x) => x, 63 | Bound::Excluded(_) => unreachable!(), 64 | Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), 65 | }; 66 | let end = match t_range.end_bound() { 67 | Bound::Included(x) | Bound::Excluded(x) => x, 68 | Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), 69 | }; 70 | let last_point_included = matches!(t_range.end_bound(), Bound::Included(_)); 71 | let increment = if last_point_included { 72 | (end - start) / (points - 1) as f64 73 | } else { 74 | (end - start) / points as f64 75 | }; 76 | let values = (0..points).map(|i| { 77 | let t = start + i as f64 * increment; 78 | let (x, y) = function(t); 79 | Value { x, y } 80 | }); 81 | Self::from_values_iter(values) 82 | } 83 | 84 | /// From a series of y-values. 85 | /// The x-values will be the indices of these values 86 | pub fn from_ys_f32(ys: &[f32]) -> Self { 87 | let values: Vec = ys 88 | .iter() 89 | .enumerate() 90 | .map(|(i, &y)| Value { 91 | x: i as f64, 92 | y: y as f64, 93 | }) 94 | .collect(); 95 | Self::from_values(values) 96 | } 97 | 98 | /// Returns true if there are no data points available and there is no 99 | /// function to generate any. 100 | pub fn is_empty(&self) -> bool { 101 | self.generator.is_none() && self.values.is_empty() 102 | } 103 | 104 | /// If initialized with a generator function, this will generate `n` evenly 105 | /// spaced points in the given range. 106 | pub fn generate_points(&mut self, x_range: RangeInclusive) { 107 | if let Some(generator) = self.generator.take() { 108 | if let Some(intersection) = Self::range_intersection(&x_range, &generator.x_range) { 109 | let increment = 110 | (intersection.end() - intersection.start()) / (generator.points - 1) as f64; 111 | self.values = (0..generator.points) 112 | .map(|i| { 113 | let x = intersection.start() + i as f64 * increment; 114 | let y = (generator.function)(x); 115 | Value { x, y } 116 | }) 117 | .collect(); 118 | } 119 | } 120 | } 121 | 122 | /// Returns the intersection of two ranges if they intersect. 123 | fn range_intersection( 124 | range1: &RangeInclusive, 125 | range2: &RangeInclusive, 126 | ) -> Option> { 127 | let start = range1.start().max(*range2.start()); 128 | let end = range1.end().min(*range2.end()); 129 | (start < end).then_some(start..=end) 130 | } 131 | 132 | pub(crate) fn get_bounds(&self) -> Bounds { 133 | let mut bounds = Bounds::NOTHING; 134 | self.values 135 | .iter() 136 | .for_each(|value| bounds.extend_with(value)); 137 | bounds 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /crates/amdgui/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::Arc; 3 | 4 | pub use app::AmdGui; 5 | use parking_lot::Mutex; 6 | use tokio::sync::mpsc::UnboundedReceiver; 7 | use tracing::level_filters::LevelFilter; 8 | use tracing_subscriber::EnvFilter; 9 | 10 | pub mod app; 11 | pub mod backend; 12 | pub mod items; 13 | pub mod transform; 14 | pub mod widgets; 15 | pub use {egui, image, parking_lot}; 16 | 17 | #[derive(Debug, gumdrop::Options)] 18 | struct Opts { 19 | config: Option, 20 | } 21 | 22 | pub async fn start_app(f: F) 23 | where 24 | F: Fn(Arc>, UnboundedReceiver), 25 | { 26 | let config_path = gumdrop::parse_args_default_or_exit::() 27 | .config 28 | .unwrap_or_else(|| PathBuf::new().join(amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH)); 29 | let config_path = config_path.to_str().expect("Config path must exists"); 30 | 31 | if std::env::var("RUST_LOG").is_err() { 32 | std::env::set_var("RUST_LOG", "DEBUG"); 33 | } 34 | let config = Arc::new(Mutex::new( 35 | amdgpu_config::fan::load_config(config_path).expect("No FAN config"), 36 | )); 37 | tracing_subscriber::fmt::fmt() 38 | .with_env_filter(EnvFilter::from_default_env()) 39 | .with_max_level({ 40 | let level = config 41 | .lock() 42 | .log_level() 43 | .as_str() 44 | .parse::() 45 | .unwrap(); 46 | if cfg!(debug_assertions) { 47 | eprintln!("log level is: {level:?}"); 48 | } 49 | level 50 | }) 51 | .init(); 52 | let amd_gui = Arc::new(Mutex::new(AmdGui::new_with_config(config))); 53 | 54 | let receiver = schedule_tick(amd_gui.clone()); 55 | 56 | f(amd_gui, receiver); 57 | } 58 | 59 | fn schedule_tick(amd_gui: Arc>) -> UnboundedReceiver { 60 | let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); 61 | tokio::spawn(async move { 62 | let sender = sender; 63 | loop { 64 | amd_gui.lock().tick(); 65 | if let Err(e) = sender.send(true) { 66 | tracing::error!("Failed to propagate tick update. {:?}", e); 67 | } 68 | tokio::time::sleep(tokio::time::Duration::from_millis(166)).await; 69 | } 70 | }); 71 | receiver 72 | } 73 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/cooling_performance.rs: -------------------------------------------------------------------------------- 1 | use std::collections::vec_deque::VecDeque; 2 | 3 | use amdmond_lib::AmdMon; 4 | use egui::{Color32, Ui}; 5 | 6 | use crate::app::{FanConfig, FanServices}; 7 | 8 | pub struct CoolingPerformance { 9 | capacity: usize, 10 | data: VecDeque, 11 | amd_mon: Option, 12 | } 13 | 14 | impl CoolingPerformance { 15 | #[allow(clippy::explicit_auto_deref)] 16 | pub fn new(capacity: usize, fan_config: FanConfig) -> Self { 17 | let amd_mon = amdgpu::utils::hw_mons(true) 18 | .map_err(|e| { 19 | tracing::error!("Failed to open hw mon: {e}"); 20 | e 21 | }) 22 | .ok() 23 | .and_then(|mut v| { 24 | if v.is_empty() { 25 | None 26 | } else { 27 | Some(v.remove(0)) 28 | } 29 | }) 30 | .map(|c| AmdMon::wrap(c, &*fan_config.lock())); 31 | 32 | Self { 33 | capacity, 34 | data: VecDeque::with_capacity(capacity), 35 | amd_mon, 36 | } 37 | } 38 | 39 | pub fn tick(&mut self) { 40 | if let Some(temp) = self 41 | .amd_mon 42 | .as_ref() 43 | .and_then(|mon| mon.gpu_temp_of(0)) 44 | .and_then(|(_, value)| value.ok()) 45 | { 46 | self.push(temp); 47 | } 48 | } 49 | 50 | pub fn draw(&self, ui: &mut Ui, pid_files: &FanServices) { 51 | use egui_plot::*; 52 | 53 | let current = self.data.iter().last().copied().unwrap_or_default(); 54 | 55 | let iter = self 56 | .data 57 | .iter() 58 | .enumerate() 59 | .map(|(i, v)| [i as f64, *v]) 60 | .collect(); 61 | 62 | let curve = Line::new(PlotPoints::new(iter)).color(Color32::BLUE); 63 | let zero = HLine::new(0.0).color(Color32::from_white_alpha(0)); 64 | let optimal = HLine::new(45.0).name("Optimal").color(Color32::LIGHT_BLUE); 65 | let target = HLine::new(80.0) 66 | .name("Overheating") 67 | .color(Color32::DARK_RED); 68 | 69 | ui.label("Temperature"); 70 | Plot::new("cooling performance") 71 | .allow_drag(false) 72 | .allow_zoom(false) 73 | .height(600.0) 74 | .show(ui, |plot_ui| { 75 | // 76 | plot_ui.line(curve); 77 | plot_ui.hline(zero); 78 | plot_ui.hline(optimal); 79 | plot_ui.hline(target); 80 | // plot_ui.legend(Legend::default()); 81 | }); 82 | 83 | // ui.add(plot); 84 | ui.horizontal(|ui| { 85 | ui.label("Current temperature"); 86 | ui.label(format!("{:<3.2}°C", current)); 87 | }); 88 | ui.label("Working services"); 89 | if pid_files.0.is_empty() { 90 | ui.label(" There's no working services"); 91 | } else { 92 | pid_files.0.iter().for_each(|service| { 93 | ui.label(format!(" {}", service.pid.0)); 94 | }); 95 | } 96 | } 97 | 98 | pub fn push(&mut self, v: f64) { 99 | if self.data.len() >= self.capacity { 100 | self.data.pop_front(); 101 | } 102 | self.data.push_back(v); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/legend.rs: -------------------------------------------------------------------------------- 1 | use egui::epaint::CircleShape; 2 | use egui::{ 3 | pos2, vec2, Align, Color32, PointerButton, Rect, Response, Sense, Shape, TextStyle, WidgetInfo, 4 | WidgetType, 5 | }; 6 | 7 | /// Where to place the plot legend. 8 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 9 | pub enum Corner { 10 | LeftTop, 11 | RightTop, 12 | LeftBottom, 13 | RightBottom, 14 | } 15 | 16 | impl Corner { 17 | pub fn all() -> impl Iterator { 18 | [ 19 | Corner::LeftTop, 20 | Corner::RightTop, 21 | Corner::LeftBottom, 22 | Corner::RightBottom, 23 | ] 24 | .iter() 25 | .copied() 26 | } 27 | } 28 | 29 | /// The configuration for a plot legend. 30 | #[derive(Clone, PartialEq)] 31 | pub struct Legend { 32 | pub text_style: TextStyle, 33 | pub background_alpha: f32, 34 | pub position: Corner, 35 | } 36 | 37 | impl Default for Legend { 38 | fn default() -> Self { 39 | Self { 40 | text_style: TextStyle::Body, 41 | background_alpha: 0.75, 42 | position: Corner::RightTop, 43 | } 44 | } 45 | } 46 | 47 | impl Legend { 48 | /// Which text style to use for the legend. Default: `TextStyle::Body`. 49 | #[must_use] 50 | pub fn text_style(mut self, style: TextStyle) -> Self { 51 | self.text_style = style; 52 | self 53 | } 54 | 55 | /// The alpha of the legend background. Default: `0.75`. 56 | #[must_use] 57 | pub fn background_alpha(mut self, alpha: f32) -> Self { 58 | self.background_alpha = alpha; 59 | self 60 | } 61 | 62 | /// In which corner to place the legend. Default: `Corner::RightTop`. 63 | #[must_use] 64 | pub fn position(mut self, corner: Corner) -> Self { 65 | self.position = corner; 66 | self 67 | } 68 | } 69 | 70 | #[derive(Clone)] 71 | pub struct LegendEntry { 72 | pub color: Color32, 73 | pub checked: bool, 74 | pub hovered: bool, 75 | } 76 | 77 | impl LegendEntry { 78 | pub fn new(color: Color32, checked: bool) -> Self { 79 | Self { 80 | color, 81 | checked, 82 | hovered: false, 83 | } 84 | } 85 | 86 | pub fn ui(&mut self, ui: &mut egui::Ui, text: String) -> Response { 87 | let Self { 88 | color, 89 | checked, 90 | hovered, 91 | } = self; 92 | 93 | let galley = ui.fonts(|reader| { 94 | reader.layout_delayed_color( 95 | text, 96 | ui.style() 97 | .text_styles 98 | .get(&TextStyle::Body) 99 | .unwrap() 100 | .clone(), 101 | f32::INFINITY, 102 | ) 103 | }); 104 | 105 | let icon_size = galley.size().y; 106 | let icon_spacing = icon_size / 5.0; 107 | let total_extra = vec2(icon_size + icon_spacing, 0.0); 108 | 109 | let desired_size = total_extra + galley.size(); 110 | let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); 111 | 112 | response.widget_info(|| { 113 | WidgetInfo::selected(WidgetType::Checkbox, true, *checked, galley.text()) 114 | }); 115 | 116 | let visuals = ui.style().interact(&response); 117 | let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT; 118 | 119 | let icon_position_x = if label_on_the_left { 120 | rect.right() - icon_size / 2.0 121 | } else { 122 | rect.left() + icon_size / 2.0 123 | }; 124 | let icon_position = pos2(icon_position_x, rect.center().y); 125 | let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); 126 | 127 | let painter = ui.painter(); 128 | 129 | painter.add(CircleShape { 130 | center: icon_rect.center(), 131 | radius: icon_size * 0.5, 132 | fill: visuals.bg_fill, 133 | stroke: visuals.bg_stroke, 134 | }); 135 | 136 | if *checked { 137 | let fill = if *color == Color32::TRANSPARENT { 138 | ui.visuals().noninteractive().fg_stroke.color 139 | } else { 140 | *color 141 | }; 142 | painter.add(Shape::circle_filled( 143 | icon_rect.center(), 144 | icon_size * 0.4, 145 | fill, 146 | )); 147 | } 148 | 149 | let text_position_x = if label_on_the_left { 150 | rect.right() - icon_size - icon_spacing - galley.size().x 151 | } else { 152 | rect.left() + icon_size + icon_spacing 153 | }; 154 | 155 | let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y); 156 | painter.galley_with_override_text_color(text_position, galley, visuals.text_color()); 157 | 158 | *checked ^= response.clicked_by(PointerButton::Primary); 159 | *hovered = response.hovered(); 160 | 161 | response 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/legend_widget.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use egui::epaint::ahash::AHashSet; 4 | use egui::epaint::Shadow; 5 | use egui::{Align, Color32, Direction, Frame, Layout, Rect, Response, Ui, Widget}; 6 | 7 | // use egui::style::Margin; 8 | // use epaint::margin::Margin; 9 | use crate::items::PlotItem; 10 | use crate::widgets::legend::{Corner, Legend, LegendEntry}; 11 | 12 | #[derive(Clone)] 13 | pub struct LegendWidget { 14 | rect: Rect, 15 | entries: BTreeMap, 16 | config: Legend, 17 | } 18 | 19 | impl LegendWidget { 20 | /// Create a new legend from items, the names of items that are hidden and 21 | /// the style of the text. Returns `None` if the legend has no entries. 22 | pub fn try_new( 23 | rect: Rect, 24 | config: Legend, 25 | items: &[Box], 26 | hidden_items: &AHashSet, 27 | ) -> Option { 28 | let mut entries: BTreeMap = BTreeMap::new(); 29 | items 30 | .iter() 31 | .filter(|item| !item.name().is_empty()) 32 | .for_each(|item| { 33 | entries 34 | .entry(item.name().to_string()) 35 | .and_modify(|entry| { 36 | if entry.color != item.color() { 37 | // Multiple items with different colors 38 | entry.color = Color32::TRANSPARENT; 39 | } 40 | }) 41 | .or_insert_with(|| { 42 | let color = item.color(); 43 | let checked = !hidden_items.contains(item.name()); 44 | LegendEntry::new(color, checked) 45 | }); 46 | }); 47 | (!entries.is_empty()).then_some(Self { 48 | rect, 49 | entries, 50 | config, 51 | }) 52 | } 53 | 54 | // Get the names of the hidden items. 55 | pub fn get_hidden_items(&self) -> AHashSet { 56 | self.entries 57 | .iter() 58 | .filter(|(_, entry)| !entry.checked) 59 | .map(|(name, _)| name.clone()) 60 | .collect() 61 | } 62 | 63 | // Get the name of the hovered items. 64 | pub fn get_hovered_entry_name(&self) -> Option { 65 | self.entries 66 | .iter() 67 | .find(|(_, entry)| entry.hovered) 68 | .map(|(name, _)| name.to_string()) 69 | } 70 | } 71 | 72 | impl Widget for &mut LegendWidget { 73 | fn ui(self, ui: &mut Ui) -> Response { 74 | let LegendWidget { 75 | rect, 76 | entries, 77 | config, 78 | } = self; 79 | 80 | let main_dir = match config.position { 81 | Corner::LeftTop | Corner::RightTop => Direction::TopDown, 82 | Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp, 83 | }; 84 | let cross_align = match config.position { 85 | Corner::LeftTop | Corner::LeftBottom => Align::LEFT, 86 | Corner::RightTop | Corner::RightBottom => Align::RIGHT, 87 | }; 88 | let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); 89 | let legend_pad = 4.0; 90 | let legend_rect = rect.shrink(legend_pad); 91 | let mut legend_ui = ui.child_ui(legend_rect, layout, None); 92 | legend_ui 93 | .scope(|ui| { 94 | // ui.style_mut().body_text_style = config.text_style; 95 | let background_frame = Frame { 96 | // inner_margin: Margin::symmetric(8.0, 4.0), 97 | outer_margin: Default::default(), 98 | shadow: Shadow::default(), 99 | fill: ui.style().visuals.extreme_bg_color, 100 | stroke: ui.style().visuals.window_stroke(), 101 | // rounding: ui.style().visuals.window_rounding, 102 | ..Default::default() 103 | } 104 | .multiply_with_opacity(config.background_alpha); 105 | background_frame 106 | .show(ui, |ui| { 107 | entries 108 | .iter_mut() 109 | .map(|(name, entry)| entry.ui(ui, name.clone())) 110 | .reduce(|r1, r2| r1.union(r2)) 111 | .unwrap() 112 | }) 113 | .inner 114 | }) 115 | .inner 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cooling_performance; 2 | pub mod drag_plot; 3 | pub mod drag_plot_prepared; 4 | pub mod edit_temp_config; 5 | pub mod edit_usage_config; 6 | pub mod legend; 7 | pub mod legend_widget; 8 | pub mod output_widget; 9 | pub mod outputs_settings; 10 | pub mod reload_section; 11 | pub mod temp_config_file; 12 | pub mod usage_config_file; 13 | 14 | pub use cooling_performance::*; 15 | pub use edit_temp_config::*; 16 | pub use edit_usage_config::*; 17 | pub use temp_config_file::*; 18 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/output_widget.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::pidfile::ports::Output; 2 | use egui::*; 3 | 4 | use crate::app::StatefulConfig; 5 | 6 | pub struct OutputWidget<'output, 'stateful> { 7 | output: &'output Output, 8 | state: &'stateful mut StatefulConfig, 9 | } 10 | 11 | impl<'output, 'stateful> OutputWidget<'output, 'stateful> { 12 | pub fn new(output: &'output Output, state: &'stateful mut StatefulConfig) -> Self { 13 | Self { output, state } 14 | } 15 | } 16 | 17 | impl<'output, 'stateful> egui::Widget for OutputWidget<'output, 'stateful> { 18 | fn ui(self, ui: &mut Ui) -> Response { 19 | let (rect, res) = ui.allocate_exact_size(Vec2::new(80.0, 80.0), Sense::click()); 20 | if let Some(handle) = self.output.ty.and_then(|ty| self.state.textures.get(&ty)) { 21 | ui.image((handle.id(), handle.size_vec2())); 22 | } else { 23 | let painter = ui.painter(); 24 | painter.rect_filled(rect, 0.0, Color32::DARK_RED); 25 | painter.rect( 26 | rect, 27 | 2.0, 28 | Color32::DARK_RED, 29 | Stroke { 30 | width: 1.0, 31 | color: Color32::GREEN, 32 | }, 33 | StrokeKind::Inside, 34 | ); 35 | 36 | let rect_middle_point = (rect.max - rect.min) / 2.0; 37 | 38 | painter.circle_filled( 39 | rect.min + Vec2::new(rect_middle_point.x / 2.0, rect_middle_point.y), 40 | 3.0, 41 | Color32::GREEN, 42 | ); 43 | painter.circle_filled(rect.min + rect_middle_point, 3.0, Color32::GREEN); 44 | 45 | painter.circle_filled( 46 | rect.min 47 | + Vec2::new( 48 | rect_middle_point.x + (rect_middle_point.x / 2.0), 49 | rect_middle_point.y, 50 | ), 51 | 3.0, 52 | Color32::GREEN, 53 | ); 54 | } 55 | res 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/outputs_settings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use amdgpu::pidfile::ports::{Output, Status}; 4 | use egui::{Color32, RichText, Ui, WidgetText}; 5 | 6 | use crate::app::StatefulConfig; 7 | use crate::widgets::output_widget::OutputWidget; 8 | 9 | #[derive(Default)] 10 | pub struct OutputsSettings {} 11 | 12 | impl OutputsSettings { 13 | pub fn draw( 14 | &mut self, 15 | ui: &mut Ui, 16 | state: &mut StatefulConfig, 17 | outputs: &BTreeMap>, 18 | ) { 19 | let _available = ui.available_rect_before_wrap(); 20 | 21 | ui.vertical(|ui| { 22 | ui.horizontal_top(|ui| { 23 | outputs.iter().for_each(|(name, outputs)| { 24 | ui.vertical(|ui| { 25 | ui.label(format!("Card {name}")); 26 | ui.horizontal_top(|ui| { 27 | outputs.iter().for_each(|output| { 28 | Self::render_single(ui, state, output); 29 | }); 30 | }); 31 | }); 32 | }); 33 | }); 34 | }); 35 | } 36 | 37 | fn render_single(ui: &mut Ui, state: &mut StatefulConfig, output: &Output) { 38 | ui.vertical(|ui| { 39 | ui.add(OutputWidget::new(output, state)); 40 | 41 | ui.label(format!("Port type {:?}", output.port_type)); 42 | ui.label(format!("Port number {}", output.port_number)); 43 | if let Some(name) = output.port_name.as_deref() { 44 | ui.label(format!("Port name {}", name)); 45 | } 46 | 47 | ui.label(WidgetText::RichText( 48 | RichText::new(match output.status { 49 | Status::Connected => "Connected", 50 | Status::Disconnected => "Disconnected", 51 | }) 52 | .color(match output.status { 53 | Status::Connected => Color32::GREEN, 54 | Status::Disconnected => Color32::GRAY, 55 | }) 56 | .code() 57 | .strong() 58 | .monospace(), 59 | )); 60 | ui.label("Display Power Management"); 61 | ui.label(WidgetText::RichText( 62 | RichText::new(match output.display_power_managment { 63 | true => "On", 64 | false => "Off", 65 | }) 66 | .color(match output.display_power_managment { 67 | true => Color32::GREEN, 68 | false => Color32::GRAY, 69 | }) 70 | .monospace() 71 | .code() 72 | .strong(), 73 | )); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/reload_section.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::pidfile::helper_cmd::Command; 2 | use egui::{PointerButton, Response, Sense, Ui}; 3 | 4 | use crate::app::{ChangeState, FanServices}; 5 | 6 | pub struct ReloadSection<'l> { 7 | pub services: &'l mut FanServices, 8 | } 9 | 10 | impl<'l> egui::Widget for ReloadSection<'l> { 11 | fn ui(self, ui: &mut Ui) -> Response { 12 | ui.vertical(|ui| { 13 | ui.label("Reload config for service"); 14 | 15 | self.services.0.iter_mut().for_each(|service| { 16 | ui.horizontal(|ui| { 17 | ui.label(format!("PID {}", service.pid.0)); 18 | if ui.button("Reload").clicked_by(PointerButton::Primary) { 19 | service.reload = ChangeState::Reloading; 20 | 21 | match amdgpu::pidfile::helper_cmd::send_command(Command::ReloadConfig { 22 | pid: service.pid, 23 | }) { 24 | Ok(response) => { 25 | service.reload = ChangeState::Success; 26 | tracing::info!("{:?}", response) 27 | } 28 | Err(e) => { 29 | service.reload = ChangeState::Failure(format!("{:?}", e)); 30 | tracing::error!("Failed to reload config. {:?}", e) 31 | } 32 | } 33 | } 34 | match &service.reload { 35 | ChangeState::New => {} 36 | ChangeState::Reloading => { 37 | ui.label("Reloading..."); 38 | } 39 | ChangeState::Success => { 40 | ui.add( 41 | egui::Label::new("Reloaded"), /* .text_color(Color32::DARK_GREEN) */ 42 | ); 43 | } 44 | ChangeState::Failure(msg) => { 45 | ui.add( 46 | egui::Label::new(format!("Failure. {}", msg)), // .text_color(Color32::RED), 47 | ); 48 | } 49 | } 50 | }); 51 | }); 52 | }); 53 | ui.allocate_response(ui.available_size(), Sense::click()) 54 | } 55 | } 56 | 57 | impl<'l> ReloadSection<'l> { 58 | pub fn new(services: &'l mut FanServices) -> Self { 59 | Self { services } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/temp_config_file.rs: -------------------------------------------------------------------------------- 1 | use amdgpu_config::fan::TempPoint; 2 | use egui::{PointerButton, Response, Sense, Ui, Widget}; 3 | 4 | use crate::app::FanConfig; 5 | 6 | pub struct TempConfigFile<'l> { 7 | config: FanConfig, 8 | matrix: &'l mut [TempPoint], 9 | } 10 | 11 | impl<'l> TempConfigFile<'l> { 12 | pub fn new(config: FanConfig, matrix: &'l mut [TempPoint]) -> Self { 13 | Self { config, matrix } 14 | } 15 | } 16 | 17 | impl<'l> Widget for TempConfigFile<'l> { 18 | fn ui(self, ui: &mut Ui) -> Response { 19 | let config = self.config.clone(); 20 | 21 | ui.vertical(|ui| { 22 | let mut matrix = self.matrix.iter_mut().enumerate().peekable(); 23 | 24 | let mut prev: Option = None; 25 | 26 | while let Some((idx, current)) = matrix.next() { 27 | let min: TempPoint = if current == &TempPoint::MIN { 28 | TempPoint::MIN 29 | } else if let Some(prev) = &prev { 30 | *prev 31 | } else { 32 | TempPoint::MIN 33 | }; 34 | let next = matrix.peek(); 35 | let max: TempPoint = if current == &TempPoint::MAX { 36 | TempPoint::MAX 37 | } else if let Some(next) = next.map(|(_, n)| n) { 38 | TempPoint::new(next.temp, next.speed) 39 | } else { 40 | TempPoint::MAX 41 | }; 42 | 43 | { 44 | ui.label("Speed"); 45 | if ui 46 | .add(egui::Slider::new(&mut current.speed, min.speed..=max.speed)) 47 | .changed() 48 | { 49 | if let Some(entry) = config.lock().temp_matrix_mut().get_mut(idx) { 50 | entry.speed = current.speed; 51 | } 52 | } 53 | } 54 | { 55 | ui.label("Temperature"); 56 | if ui 57 | .add(egui::Slider::new(&mut current.temp, min.temp..=max.temp)) 58 | .changed() 59 | { 60 | if let Some(entry) = config.lock().temp_matrix_mut().get_mut(idx) { 61 | entry.temp = current.temp; 62 | } 63 | } 64 | } 65 | 66 | ui.horizontal(|ui| { 67 | if next.is_some() { 68 | if ui 69 | .add(egui::Button::new("Add in the middle")) 70 | .clicked_by(PointerButton::Primary) 71 | { 72 | config.lock().temp_matrix_vec_mut().insert( 73 | idx + 1, 74 | TempPoint::new( 75 | min.speed + ((max.speed - min.speed) / 2.0), 76 | min.temp + ((max.temp - min.temp) / 2.0), 77 | ), 78 | ) 79 | } 80 | } else if next.is_none() 81 | && *current != TempPoint::MAX 82 | && ui 83 | .add(egui::Button::new("Add")) 84 | .clicked_by(PointerButton::Primary) 85 | { 86 | config 87 | .lock() 88 | .temp_matrix_vec_mut() 89 | .push(TempPoint::new(100.0, 100.0)) 90 | } 91 | if ui 92 | .add(egui::Button::new("Remove")) 93 | .clicked_by(PointerButton::Primary) 94 | { 95 | config.lock().temp_matrix_vec_mut().remove(idx); 96 | } 97 | }); 98 | 99 | ui.separator(); 100 | prev = Some(*current); 101 | } 102 | 103 | ui.allocate_response(ui.available_size(), Sense::click()) 104 | }) 105 | .inner 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/amdgui/src/widgets/usage_config_file.rs: -------------------------------------------------------------------------------- 1 | use amdgpu_config::fan::UsagePoint; 2 | use egui::{PointerButton, Response, Sense, Ui, Widget}; 3 | 4 | use crate::app::FanConfig; 5 | 6 | pub struct UsageConfigFile { 7 | config: FanConfig, 8 | } 9 | 10 | impl UsageConfigFile { 11 | pub fn new(config: FanConfig) -> Self { 12 | Self { config } 13 | } 14 | } 15 | 16 | impl Widget for UsageConfigFile { 17 | fn ui(self, ui: &mut Ui) -> Response { 18 | let c = self.config; 19 | 20 | ui.vertical(|ui| { 21 | let mut l = c.lock(); 22 | let matrix = l.usage_matrix_vec_mut(); 23 | let mut iter = matrix.clone().into_iter().enumerate().peekable(); 24 | 25 | let mut prev: Option = None; 26 | 27 | while let Some((idx, mut current)) = iter.next() { 28 | let min: UsagePoint = match prev { 29 | _ if current == UsagePoint::MIN => UsagePoint::MIN, 30 | Some(p) => p, 31 | _ => UsagePoint::MIN, 32 | }; 33 | 34 | let next = iter.peek(); 35 | let max = match next { 36 | _ if current == UsagePoint::MAX => UsagePoint::MAX, 37 | Some((_, next)) => UsagePoint::new(next.usage, next.speed), 38 | _ => UsagePoint::MAX, 39 | }; 40 | 41 | { 42 | ui.label("Usage"); 43 | if ui 44 | .add(egui::Slider::new(&mut current.speed, min.speed..=max.speed)) 45 | .changed() 46 | { 47 | if let Some(entry) = matrix.get_mut(idx) { 48 | entry.speed = current.speed; 49 | } 50 | } 51 | } 52 | { 53 | ui.label("Temperature"); 54 | if ui 55 | .add(egui::Slider::new(&mut current.usage, min.usage..=max.usage)) 56 | .changed() 57 | { 58 | if let Some(entry) = matrix.get_mut(idx) { 59 | entry.usage = current.usage; 60 | } 61 | } 62 | } 63 | 64 | ui.horizontal(|ui| { 65 | if next.is_some() { 66 | if ui 67 | .add(egui::Button::new("Add in the middle")) 68 | .clicked_by(PointerButton::Primary) 69 | { 70 | matrix.insert( 71 | idx + 1, 72 | UsagePoint::new( 73 | min.speed + ((max.speed - min.speed) / 2.0), 74 | min.usage + ((max.usage - min.usage) / 2.0), 75 | ), 76 | ) 77 | } 78 | } else if next.is_none() 79 | && current != UsagePoint::MAX 80 | && ui 81 | .add(egui::Button::new("Add")) 82 | .clicked_by(PointerButton::Primary) 83 | { 84 | matrix.push(UsagePoint::new(100.0, 100.0)) 85 | } 86 | if ui 87 | .add(egui::Button::new("Remove")) 88 | .clicked_by(PointerButton::Primary) 89 | { 90 | matrix.remove(idx); 91 | } 92 | }); 93 | 94 | ui.separator(); 95 | prev = Some(current); 96 | } 97 | 98 | ui.allocate_response(ui.available_size(), Sense::click()) 99 | }) 100 | .inner 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/amdguid-client/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-gnu" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdguid-client/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-gnu" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdguid-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdgui-client" 3 | version = "1.0.14" 4 | edition = "2018" 5 | description = "AMDGPU GUI client" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | depends = ["amdgui-helper"] 15 | 16 | [[bin]] 17 | name = "agc" 18 | path = "./src/main.rs" 19 | 20 | [dependencies] 21 | amdgpu = { path = "../amdgpu", version = "1", features = ["gui-helper"] } 22 | amdgpu-config = { path = "../amdgpu-config", version = "1", features = ["fan", "gui"] } 23 | amdguid = { path = "../amdgui", version = "1" } 24 | amdmond-lib = { path = "../amdmond-lib", version = "1" } 25 | bytemuck = { workspace = true } 26 | eframe = { workspace = true } 27 | egui = { workspace = true } 28 | egui-winit = { workspace = true } 29 | emath = { workspace = true } 30 | gumdrop = { workspace = true } 31 | nix = { workspace = true } 32 | serde = { workspace = true, features = ["derive"] } 33 | thiserror = { workspace = true } 34 | tokio = { workspace = true, features = ["full"] } 35 | toml = { workspace = true } 36 | tracing = { workspace = true } 37 | 38 | [dev-dependencies] 39 | amdgpu = { path = "../amdgpu", version = "1.0", features = ["gui-helper"] } 40 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["fan", "gui"] } 41 | amdmond-lib = { path = "../amdmond-lib", version = "1.0" } 42 | -------------------------------------------------------------------------------- /crates/amdguid-client/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdguid-client/README.md: -------------------------------------------------------------------------------- 1 | # AMD GPU gui tool 2 | 3 | Provides basic FAN configuration. 4 | 5 | ## Roadmap 6 | 7 | * amdvold config manipulation 8 | * Fix Drag & drop functionality - mouse is not followed properly 9 | * Program profiles 10 | 11 | ## Screenshots 12 | 13 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/config.png) 14 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/monitoring.png) 15 | ![Alt text](https://static.ita-prog.pl/amdgpud/assets/settings.png) 16 | -------------------------------------------------------------------------------- /crates/amdguid-client/assets/icons/html_port-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdguid-client/assets/icons/html_port-512.png -------------------------------------------------------------------------------- /crates/amdguid-client/assets/icons/ports.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdguid-client/assets/icons/ports.jpg -------------------------------------------------------------------------------- /crates/amdguid-client/assets/icons/ports2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdguid-client/assets/icons/ports2.jpg -------------------------------------------------------------------------------- /crates/amdguid-client/assets/icons/ports2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdguid-client/assets/icons/ports2.png -------------------------------------------------------------------------------- /crates/amdguid-client/assets/icons/vga_port-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Eraden/amdgpud/dd1de6b009a76383950585eddce59c48415acb53/crates/amdguid-client/assets/icons/vga_port-512.png -------------------------------------------------------------------------------- /crates/amdguid-client/src/backend.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use amdguid::parking_lot::Mutex; 4 | use amdguid::AmdGui; 5 | use tokio::sync::mpsc::UnboundedReceiver; 6 | 7 | pub fn run_app(amd_gui: Arc>, _receiver: UnboundedReceiver) { 8 | eframe::run_native( 9 | "Amd GPU Client", 10 | eframe::NativeOptions { 11 | ..Default::default() 12 | }, 13 | Box::new(|_cc| Ok(Box::new(MyApp { amd_gui }))), 14 | ) 15 | .expect("AMD GUID failed"); 16 | } 17 | 18 | struct MyApp { 19 | amd_gui: Arc>, 20 | } 21 | 22 | impl eframe::App for MyApp { 23 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 24 | amdguid::backend::create_ui(self.amd_gui.clone(), ctx); 25 | ctx.request_repaint_after(std::time::Duration::from_millis(1000 / 60)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crates/amdguid-client/src/main.rs: -------------------------------------------------------------------------------- 1 | mod backend; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | amdguid::start_app(|amd_gui, receiver| { 6 | backend::run_app(amd_gui, receiver); 7 | }) 8 | .await; 9 | } 10 | -------------------------------------------------------------------------------- /crates/amdmond-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdmond-lib" 3 | version = "1.0.11" 4 | edition = "2021" 5 | description = "AMD GPU monitoring tool for Linux" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | 11 | [dependencies] 12 | amdgpu = { path = "../amdgpu", version = "1.0.11" } 13 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["monitor", "fan"] } 14 | chrono = { workspace = true, features = ["serde"] } 15 | csv = { workspace = true } 16 | gumdrop = { workspace = true } 17 | serde = { workspace = true, features = ["derive"] } 18 | thiserror = { workspace = true } 19 | toml = { workspace = true } 20 | tracing = { workspace = true } 21 | 22 | [dev-dependencies] 23 | amdgpu = { path = "../amdgpu", version = "1.0" } 24 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] } 25 | -------------------------------------------------------------------------------- /crates/amdmond-lib/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdmond-lib/src/errors.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils; 2 | use amdgpu_config::{fan, monitor}; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | pub enum AmdMonError { 6 | #[error("Mon AMD GPU card was found")] 7 | NoHwMon, 8 | #[error("Failed to access {path:?}. {io}")] 9 | Io { io: std::io::Error, path: String }, 10 | #[error("{0}")] 11 | MonConfigError(#[from] monitor::ConfigError), 12 | #[error("{0}")] 13 | FanConfigError(#[from] fan::ConfigError), 14 | #[error("{0}")] 15 | AmdUtils(#[from] utils::AmdGpuError), 16 | #[error("{0}")] 17 | Csv(#[from] csv::Error), 18 | #[error("AMD GPU temperature is malformed. It should be number. {0:?}")] 19 | NonIntTemp(std::num::ParseIntError), 20 | #[error("AMD GPU fan speed is malformed. It should be number. {0:?}")] 21 | NonIntPwm(std::num::ParseIntError), 22 | #[error("Monitor format is not valid. Available values are: short, s, long l, verbose and v")] 23 | InvalidMonitorFormat, 24 | #[error("Failed to read AMD GPU temperatures from tempX_input. No input was found")] 25 | EmptyTempSet, 26 | } 27 | -------------------------------------------------------------------------------- /crates/amdmond-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | 3 | use amdgpu::hw_mon::HwMon; 4 | use amdgpu::utils::load_temp_inputs; 5 | use amdgpu::{ 6 | TempInput, PULSE_WIDTH_MODULATION, PULSE_WIDTH_MODULATION_MAX, PULSE_WIDTH_MODULATION_MIN, 7 | }; 8 | use amdgpu_config::fan; 9 | 10 | use crate::errors::AmdMonError; 11 | 12 | pub type Result = std::result::Result; 13 | 14 | pub struct AmdMon { 15 | temp_input: Option, 16 | inputs: Vec, 17 | hw_mon: HwMon, 18 | /// Minimal modulation (between 0-255) 19 | pub pwm_min: Option, 20 | /// Maximal modulation (between 0-255) 21 | pub pwm_max: Option, 22 | } 23 | 24 | impl std::ops::Deref for AmdMon { 25 | type Target = HwMon; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.hw_mon 29 | } 30 | } 31 | 32 | impl AmdMon { 33 | pub fn wrap_all(mons: Vec, config: &fan::Config) -> Vec { 34 | mons.into_iter() 35 | .map(|hw_mon| Self::wrap(hw_mon, config)) 36 | .collect() 37 | } 38 | 39 | pub fn wrap(hw_mon: HwMon, config: &fan::Config) -> Self { 40 | Self { 41 | temp_input: config.temp_input().cloned(), 42 | inputs: load_temp_inputs(&hw_mon), 43 | hw_mon, 44 | pwm_min: None, 45 | pwm_max: None, 46 | } 47 | } 48 | 49 | /// Returns name and gpu temperature percent of maximum GPU temperature 50 | /// range 51 | pub fn gpu_temp(&self) -> Vec<(&String, Result)> { 52 | self.inputs 53 | .iter() 54 | .map(|name| { 55 | let temp = self 56 | .read_gpu_temp(name.as_str()) 57 | .map(|temp| temp as f64 / 1000f64); 58 | (name, temp) 59 | }) 60 | .collect() 61 | } 62 | 63 | /// GPU usage percent 64 | pub fn gpu_usage(&self) -> Result { 65 | if let Some(input) = self.temp_input.as_ref() { 66 | let value = self.read_gpu_usage(&input.as_string())?; 67 | return Ok(value as f64 / 1000f64); 68 | } 69 | Ok(0.0) 70 | } 71 | 72 | pub fn gpu_temp_of(&self, input_idx: usize) -> Option<(&String, Result)> { 73 | self.inputs.get(input_idx).map(|name| { 74 | let temp = self 75 | .read_gpu_temp(name.as_str()) 76 | .map(|temp| temp as f64 / 1000f64); 77 | (name, temp) 78 | }) 79 | } 80 | 81 | pub fn read_gpu_temp(&self, name: &str) -> Result { 82 | let value = self 83 | .hw_mon_read(name)? 84 | .parse::() 85 | .map_err(AmdMonError::NonIntTemp)?; 86 | Ok(value) 87 | } 88 | 89 | pub fn gpu_usage_of(&self, input_idx: usize) -> Option<(&String, Result)> { 90 | self.inputs.get(input_idx).map(|name| { 91 | let usage = self.read_gpu_usage(name).map(|usage| usage as f64 / 100f64); 92 | (name, usage) 93 | }) 94 | } 95 | 96 | pub fn read_gpu_usage(&self, name: &str) -> Result { 97 | let value = self 98 | .hw_mon_read(name)? 99 | .parse() 100 | .map_err(AmdMonError::NonIntTemp)?; 101 | Ok(value) 102 | } 103 | 104 | pub fn pwm(&self) -> Result { 105 | let value = self 106 | .hw_mon_read(PULSE_WIDTH_MODULATION)? 107 | .parse() 108 | .map_err(AmdMonError::NonIntPwm)?; 109 | Ok(value) 110 | } 111 | 112 | pub fn pwm_min(&mut self) -> u32 { 113 | if self.pwm_min.is_none() { 114 | self.pwm_min = Some(self.value_or(PULSE_WIDTH_MODULATION_MIN, 0)); 115 | }; 116 | self.pwm_min.unwrap_or_default() 117 | } 118 | 119 | pub fn pwm_max(&mut self) -> u32 { 120 | if self.pwm_max.is_none() { 121 | self.pwm_max = Some(self.value_or(PULSE_WIDTH_MODULATION_MAX, 255)); 122 | }; 123 | self.pwm_max.unwrap_or(255) 124 | } 125 | 126 | pub fn max_gpu_temp(&self) -> Result { 127 | if let Some(input) = self.temp_input.as_ref() { 128 | let value = self.read_gpu_temp(&input.as_string())?; 129 | return Ok(value as f64 / 1000f64); 130 | } 131 | let mut results = Vec::with_capacity(self.inputs.len()); 132 | for name in self.inputs.iter() { 133 | results.push(self.read_gpu_temp(name).unwrap_or(0)); 134 | } 135 | results.sort_unstable(); 136 | let value = results 137 | .last() 138 | .copied() 139 | .map(|temp| temp as f64 / 1000f64) 140 | .ok_or(AmdMonError::EmptyTempSet)?; 141 | Ok(value) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /crates/amdmond/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdmond" 3 | version = "1.0.14" 4 | edition = "2021" 5 | description = "AMD GPU monitoring tool for Linux" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | target = "x86_64-unknown-linux-musl" 15 | 16 | [[bin]] 17 | name = 'amdmond' 18 | path = "./src/main.rs" 19 | 20 | [features] 21 | static = ["eyra"] 22 | 23 | [dependencies] 24 | amdgpu = { path = "../amdgpu", version = "1.0.11" } 25 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["monitor", "fan"] } 26 | amdmond-lib = { path = "../amdmond-lib", version = "1.0.10" } 27 | chrono = { workspace = true, features = ["serde"] } 28 | csv = { workspace = true } 29 | eyra = { workspace = true, optional = true } 30 | gumdrop = { workspace = true } 31 | serde = { workspace = true, features = ["derive"] } 32 | toml = { workspace = true } 33 | tracing = { workspace = true } 34 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 35 | 36 | [dev-dependencies] 37 | amdgpu = { path = "../amdgpu", version = "1.0" } 38 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["monitor", "fan"] } 39 | amdmond-lib = { path = "../amdmond-lib", version = "1.0" } 40 | -------------------------------------------------------------------------------- /crates/amdmond/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdmond/README.md: -------------------------------------------------------------------------------- 1 | # AMD Monitoring daemon 2 | 3 | ## Watch mode 4 | 5 | Tool will check temperature and prints: 6 | 7 | * Minimal modulation 8 | * Maximal modulation 9 | * Current modulation 10 | * Current fan speed in percentage (PWD / PWD MAX * 100) 11 | * Current value of each temperature sensor (typically temp1_input is which should be observed) 12 | * GPU usage 13 | * GPU fan speed for usage 14 | 15 | > `modulation` is a value between 0-255 which indicate how fast fan should be moving 16 | 17 | ```bash 18 | /usr/bin/amdmond watch --format short 19 | ``` 20 | 21 | ### Formats 22 | 23 | There are 2 possible formats. 24 | 25 | * `short` - very compact info 26 | * `long` - more human-readable info 27 | 28 | ## Log File mode 29 | 30 | This tool can be used to track GPU temperature and amdfand speed curve management to prevent GPU card from generating 31 | unnecessary noise. 32 | 33 | It will create csv log file with: 34 | 35 | * time 36 | * temperature 37 | * card modulation 38 | * matrix point temperature 39 | * matrix point speed 40 | * usage 41 | * speed for usage 42 | 43 | ```bash 44 | /usr/bin/amdmond log_file -s /var/log/amdmon.csv 45 | ``` 46 | 47 | ## Install 48 | 49 | ```bash 50 | cargo install amdmond 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### minimal: 56 | 57 | ``` 58 | amdmond log_file -s /var/log/amdmon.csv 59 | ``` 60 | 61 | Required arguments: 62 | 63 | * `-s`, `--stat-file STAT-FILE` Full path to statistics file 64 | 65 | Optional arguments: 66 | 67 | * `-h`, `--help` Help message 68 | * `-v`, `--version` Print version 69 | * `-c`, `--config CONFIG` Config location 70 | * `-i`, `--interval INTERVAL` Time between each check. 1000 is 1s, by default 5s 71 | 72 | ## Config file 73 | 74 | ```toml 75 | # AT /etc/amdfand/monitor.toml 76 | log_level = "Error" # One of: Off Error Warn Info Debug Trace 77 | interval = 5000 # duration in milliseconds 78 | ``` 79 | -------------------------------------------------------------------------------- /crates/amdmond/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "static")] 3 | println!("cargo:rustc-link-arg=-nostartfiles"); 4 | } 5 | -------------------------------------------------------------------------------- /crates/amdmond/src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::{log_file, watch}; 2 | 3 | #[derive(gumdrop::Options)] 4 | pub enum Command { 5 | Watch(watch::Watch), 6 | LogFile(log_file::LogFile), 7 | } 8 | 9 | impl Default for Command { 10 | fn default() -> Self { 11 | Self::Watch(watch::Watch::default()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/amdmond/src/log_file.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use amdgpu::utils::hw_mons; 4 | use amdgpu_config::fan; 5 | use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH; 6 | use amdgpu_config::monitor::Config; 7 | use amdmond_lib::errors::AmdMonError; 8 | use amdmond_lib::AmdMon; 9 | use chrono::{DateTime, Local}; 10 | use csv::Writer; 11 | use tracing::info; 12 | 13 | #[derive(gumdrop::Options)] 14 | pub struct LogFile { 15 | #[options(help = "Help message")] 16 | help: bool, 17 | #[options(help = "Full path to statistics file")] 18 | stat_file: String, 19 | #[options(help = "Time between each check. 1000 is 1s, by default 5s")] 20 | interval: Option, 21 | } 22 | 23 | #[derive(Debug, serde::Serialize)] 24 | struct Stat { 25 | time: DateTime, 26 | temperature: f64, 27 | modulation: u32, 28 | speed_setting: f64, 29 | temperature_setting: f64, 30 | usage: f64, 31 | usage_speed: f64, 32 | } 33 | 34 | pub fn run(command: LogFile, config: Config) -> amdmond_lib::Result<()> { 35 | let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?; 36 | 37 | let duration = 38 | std::time::Duration::from_millis(command.interval.unwrap_or_else(|| config.interval())); 39 | info!("Updating each: {:?}", duration); 40 | 41 | let _ = std::fs::remove_file(command.stat_file.as_str()); 42 | let stat_file = std::fs::OpenOptions::new() 43 | .create(true) 44 | .append(false) 45 | .write(true) 46 | .open(command.stat_file.as_str()) 47 | .map_err(|io| AmdMonError::Io { 48 | io, 49 | path: command.stat_file.clone(), 50 | })?; 51 | 52 | let mut writer = csv::WriterBuilder::new() 53 | .double_quote(true) 54 | .has_headers(true) 55 | .buffer_capacity(100) 56 | .from_writer(stat_file); 57 | 58 | let mon = { 59 | let mons = hw_mons(true)?; 60 | if mons.is_empty() { 61 | return Err(AmdMonError::NoHwMon); 62 | } 63 | AmdMon::wrap(hw_mons(true)?.remove(0), &fan_config) 64 | }; 65 | 66 | loop { 67 | let time = Local::now(); 68 | 69 | write_temperature(&fan_config, &mut writer, &mon, time, &command)?; 70 | 71 | std::thread::sleep(duration); 72 | } 73 | } 74 | 75 | fn write_temperature( 76 | fan_config: &fan::Config, 77 | writer: &mut Writer, 78 | mon: &AmdMon, 79 | time: DateTime, 80 | command: &LogFile, 81 | ) -> amdmond_lib::Result<()> { 82 | let temperature = { 83 | let mut temperatures = mon.gpu_temp(); 84 | 85 | if let Some(input) = fan_config.temp_input() { 86 | let input_name = input.as_string(); 87 | temperatures 88 | .into_iter() 89 | .find(|(name, _value)| name.as_str() == input_name.as_str()) 90 | .and_then(|(_, v)| v.ok()) 91 | .unwrap_or_default() 92 | } else if temperatures.len() > 1 { 93 | temperatures.remove(1).1.unwrap_or_default() 94 | } else { 95 | temperatures 96 | .get(0) 97 | .and_then(|(_, v)| v.as_ref().ok().copied()) 98 | .unwrap_or_default() 99 | } 100 | }; 101 | let (speed_setting, temperature_setting) = 102 | if let Some(speed_matrix_point) = fan_config.temp_matrix_point(temperature) { 103 | (speed_matrix_point.speed, speed_matrix_point.temp) 104 | } else { 105 | Default::default() 106 | }; 107 | let usage = mon.gpu_usage()?; 108 | let usage_speed = fan_config.fan_speed_for_usage(usage); 109 | 110 | let stat = Stat { 111 | time, 112 | temperature, 113 | modulation: mon.pwm()?, 114 | speed_setting, 115 | temperature_setting, 116 | usage, 117 | usage_speed, 118 | }; 119 | 120 | tracing::debug!("{:?}", stat); 121 | 122 | writer.serialize(stat)?; 123 | writer.flush().map_err(|io| AmdMonError::Io { 124 | io, 125 | path: command.stat_file.clone(), 126 | })?; 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /crates/amdmond/src/main.rs: -------------------------------------------------------------------------------- 1 | mod command; 2 | mod log_file; 3 | mod watch; 4 | 5 | use amdgpu::utils::ensure_config_dir; 6 | use amdgpu_config::monitor::{load_config, Config, DEFAULT_MONITOR_CONFIG_PATH}; 7 | use gumdrop::Options; 8 | use tracing::level_filters::LevelFilter; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | use crate::command::Command; 12 | 13 | #[cfg(feature = "static")] 14 | extern crate eyra; 15 | 16 | #[derive(gumdrop::Options)] 17 | pub struct Opts { 18 | #[options(help = "Help message")] 19 | pub help: bool, 20 | #[options(help = "Print version")] 21 | pub version: bool, 22 | #[options(help = "Config location")] 23 | pub config: Option, 24 | #[options(command)] 25 | pub command: Option, 26 | } 27 | 28 | fn run(config: Config) -> amdmond_lib::Result<()> { 29 | let opts: Opts = Opts::parse_args_default_or_exit(); 30 | 31 | if opts.version { 32 | println!("amdfand {}", env!("CARGO_PKG_VERSION")); 33 | std::process::exit(0); 34 | } 35 | match opts.command { 36 | Some(Command::Watch(w)) => watch::run(w, config), 37 | Some(Command::LogFile(l)) => log_file::run(l, config), 38 | _ => { 39 | println!("{}", ::usage()); 40 | Ok(()) 41 | } 42 | } 43 | } 44 | 45 | fn setup() -> amdmond_lib::Result<(String, Config)> { 46 | if std::env::var("RUST_LOG").is_err() { 47 | std::env::set_var("RUST_LOG", "DEBUG"); 48 | } 49 | ensure_config_dir()?; 50 | 51 | let config_path = Opts::parse_args_default_or_exit() 52 | .config 53 | .unwrap_or_else(|| DEFAULT_MONITOR_CONFIG_PATH.to_string()); 54 | let config = load_config(&config_path) 55 | .map_err(|io| amdmond_lib::errors::AmdMonError::Io { 56 | io: io.into_io(), 57 | path: config_path.as_str().into(), 58 | }) 59 | .unwrap(); 60 | println!("{config:?}"); 61 | 62 | tracing_subscriber::fmt::fmt() 63 | .with_env_filter(EnvFilter::from_default_env()) 64 | .with_max_level(config.log_level().as_str().parse::().unwrap()) 65 | .init(); 66 | 67 | Ok((config_path, config)) 68 | } 69 | 70 | fn main() -> amdmond_lib::Result<()> { 71 | let (_config_path, config) = match setup() { 72 | Ok(config) => config, 73 | Err(e) => { 74 | eprintln!("{}", e); 75 | std::process::exit(1); 76 | } 77 | }; 78 | match run(config) { 79 | Ok(()) => Ok(()), 80 | Err(e) => { 81 | eprintln!("{}", e); 82 | std::process::exit(1); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/amdmond/src/watch.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use amdgpu::utils::{hw_mons, linear_map}; 4 | use amdgpu_config::fan::DEFAULT_FAN_CONFIG_PATH; 5 | use amdgpu_config::{fan, monitor}; 6 | use amdmond_lib::errors::AmdMonError; 7 | use amdmond_lib::AmdMon; 8 | 9 | #[derive(Debug, Default)] 10 | pub enum MonitorFormat { 11 | #[default] 12 | Short, 13 | Verbose, 14 | } 15 | 16 | impl FromStr for MonitorFormat { 17 | type Err = AmdMonError; 18 | 19 | fn from_str(s: &str) -> Result { 20 | match s { 21 | "short" | "s" => Ok(MonitorFormat::Short), 22 | "verbose" | "v" | "long" | "l" => Ok(MonitorFormat::Verbose), 23 | _ => Err(AmdMonError::InvalidMonitorFormat), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, gumdrop::Options)] 29 | pub struct Watch { 30 | #[options(help = "Help message")] 31 | help: bool, 32 | #[options(help = "Monitor format")] 33 | format: MonitorFormat, 34 | } 35 | 36 | impl Default for Watch { 37 | fn default() -> Self { 38 | Self { 39 | help: false, 40 | format: MonitorFormat::Short, 41 | } 42 | } 43 | } 44 | 45 | /// Start print cards temperature and fan speed 46 | pub fn run(monitor: Watch, _config: monitor::Config) -> amdmond_lib::Result<()> { 47 | let fan_config = fan::load_config(DEFAULT_FAN_CONFIG_PATH)?; 48 | match monitor.format { 49 | MonitorFormat::Short => short(fan_config), 50 | MonitorFormat::Verbose => verbose(fan_config), 51 | } 52 | } 53 | 54 | pub fn verbose(config: fan::Config) -> amdmond_lib::Result<()> { 55 | let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config); 56 | 57 | loop { 58 | print!("{esc}[2J{esc}[1;1H", esc = 27 as char); 59 | for hw_mon in hw_mons.iter_mut() { 60 | println!("Card {:3}", hw_mon.card().to_string().replace("card", "")); 61 | println!(" MIN | MAX | PWM | %"); 62 | let min = hw_mon.pwm_min(); 63 | let max = hw_mon.pwm_max(); 64 | println!( 65 | " {:>4} | {:>4} | {:>6} | {:>3}", 66 | min, 67 | max, 68 | hw_mon 69 | .pwm() 70 | .map_or_else(|_e| String::from("FAILED"), |f| f.to_string()), 71 | (linear_map( 72 | hw_mon.pwm().unwrap_or_default() as f64, 73 | min as f64, 74 | max as f64, 75 | 0f64, 76 | 100f64, 77 | )) 78 | .round(), 79 | ); 80 | 81 | println!(); 82 | println!(" Current temperature"); 83 | hw_mon.gpu_temp().into_iter().for_each(|(name, temp)| { 84 | println!( 85 | " {:6} | {:>9.2}", 86 | name.replace("_input", ""), 87 | temp.unwrap_or_default(), 88 | ); 89 | }); 90 | } 91 | println!(); 92 | println!("> PWM may be 0 even if RPM is higher"); 93 | std::thread::sleep(std::time::Duration::from_secs(4)); 94 | } 95 | } 96 | 97 | pub fn short(config: fan::Config) -> amdmond_lib::Result<()> { 98 | let mut hw_mons = AmdMon::wrap_all(hw_mons(true)?, &config); 99 | loop { 100 | print!("{esc}[2J{esc}[1;1H", esc = 27 as char); 101 | for hw_mon in hw_mons.iter_mut() { 102 | println!( 103 | "Card {:3} | Temp | MIN | MAX | PWM | %", 104 | hw_mon.card().to_string().replace("card", "") 105 | ); 106 | let min = hw_mon.pwm_min(); 107 | let max = hw_mon.pwm_max(); 108 | println!( 109 | " | {:>5.2} | {:>4} | {:>4} | {:>4} | {:>3}", 110 | hw_mon.max_gpu_temp().unwrap_or_default(), 111 | min, 112 | max, 113 | hw_mon.pwm().unwrap_or_default(), 114 | linear_map( 115 | hw_mon.pwm().unwrap_or_default() as f64, 116 | min as f64, 117 | max as f64, 118 | 0f64, 119 | 100f64, 120 | ) 121 | .round(), 122 | ); 123 | } 124 | std::thread::sleep(std::time::Duration::from_secs(4)); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/amdportsd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdportsd" 3 | version = "0.1.1" 4 | edition = "2021" 5 | description = "AMDGPU server for listing connected GPU ports" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | target = "x86_64-unknown-linux-musl" 15 | 16 | [[bin]] 17 | name = 'amdports' 18 | path = "./src/main.rs" 19 | 20 | [features] 21 | static = ["eyra"] 22 | 23 | [dependencies] 24 | amdgpu = { path = "../amdgpu", features = ["gui-helper"], version = "1" } 25 | eyra = { workspace = true, optional = true } 26 | futures = { workspace = true, features = [] } 27 | ron = { workspace = true } 28 | serde = { workspace = true, features = ["derive"] } 29 | tokio = { workspace = true, features = ["full"] } 30 | tracing = { workspace = true } 31 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 32 | -------------------------------------------------------------------------------- /crates/amdportsd/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdportsd/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "static")] 3 | println!("cargo:rustc-link-arg=-nostartfiles"); 4 | } 5 | -------------------------------------------------------------------------------- /crates/amdportsd/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{DirEntry, Permissions}; 2 | use std::os::unix::fs::PermissionsExt; 3 | use std::os::unix::net::{UnixListener, UnixStream}; 4 | use std::sync::{Arc, Mutex}; 5 | use std::time::Duration; 6 | 7 | use amdgpu::pidfile::ports::*; 8 | use amdgpu::IoFailure; 9 | use tracing_subscriber::EnvFilter; 10 | 11 | #[cfg(feature = "static")] 12 | extern crate eyra; 13 | 14 | fn parse_output(entry: DirEntry) -> Option { 15 | let ty = entry.file_type().ok()?; 16 | if ty.is_dir() { 17 | return None; 18 | } 19 | let file_name = entry.file_name(); 20 | let path = file_name.to_str()?; 21 | let mut it = path 22 | .split('-') 23 | .map(String::from) 24 | .collect::>() 25 | .into_iter(); 26 | 27 | let modes = std::fs::read_to_string(entry.path().join("modes")) 28 | .unwrap_or_default() 29 | .lines() 30 | .filter_map(|s| { 31 | let mut it = s.split('x'); 32 | let width = it.next().and_then(|s| s.parse::().ok())?; 33 | let height = it.next().and_then(|s| s.parse::().ok())?; 34 | Some(OutputMode { width, height }) 35 | }) 36 | .collect::>(); 37 | 38 | let card = it.next()?.strip_prefix("card")?.to_string(); 39 | let port_type = it.next()?; 40 | let dpms = std::fs::read_to_string(entry.path().join("dpms")) 41 | .unwrap_or_else(|e| { 42 | tracing::error!("{}", e); 43 | "Off".into() 44 | }) 45 | .to_lowercase(); 46 | tracing::info!("Display Power Management System is {:?}", dpms); 47 | 48 | let mut output = Output { 49 | card, 50 | ty: OutputType::parse_str(&port_type), 51 | port_type, 52 | modes, 53 | display_power_managment: dpms.trim() == "on", 54 | ..Default::default() 55 | }; 56 | let mut it = it.rev(); 57 | output.port_number = it.next()?.parse().ok()?; 58 | 59 | let mut it = it.rev().peekable(); 60 | 61 | if it.peek().is_some() { 62 | output.port_name = Some(it.collect::>().join("-")); 63 | } 64 | 65 | output.status = output.read_status()?; 66 | 67 | Some(output) 68 | } 69 | 70 | async fn read_outputs(state: Arc>>) { 71 | loop { 72 | let outputs = std::fs::read_dir("/sys/class/drm") 73 | .unwrap() 74 | .filter_map(|r| r.ok()) 75 | .filter(|e| { 76 | e.path() 77 | .to_str() 78 | .map(|s| s.contains("card")) 79 | .unwrap_or_default() 80 | }) 81 | .filter_map(parse_output) 82 | .collect::>(); 83 | if let Ok(mut lock) = state.lock() { 84 | *lock = outputs; 85 | } 86 | 87 | tokio::time::sleep(Duration::from_millis(1_000 / 3)).await; 88 | } 89 | } 90 | 91 | pub type Service = amdgpu::pidfile::Service; 92 | 93 | async fn service(state: Arc>>) { 94 | let sock_path = sock_file(); 95 | let listener = { 96 | let _ = std::fs::remove_file(&sock_path); 97 | 98 | UnixListener::bind(&sock_path) 99 | .map_err(|io| IoFailure { 100 | io, 101 | path: sock_path.clone(), 102 | }) 103 | .expect("Creating pid file for ports failed") 104 | }; 105 | if let Err(e) = std::fs::set_permissions(&sock_path, Permissions::from_mode(0o777)) { 106 | tracing::error!("Failed to change gui helper socket file mode. {:?}", e); 107 | } 108 | 109 | while let Ok((stream, _addr)) = listener.accept() { 110 | handle_connection(stream, state.clone()); 111 | } 112 | } 113 | 114 | fn handle_connection(stream: UnixStream, state: Arc>>) { 115 | let mut service = Service::new(stream); 116 | 117 | let command = match service.read_command() { 118 | Some(s) => s, 119 | _ => return service.kill(), 120 | }; 121 | 122 | tracing::info!("Incoming {:?}", command); 123 | let cmd = match ron::from_str::(command.trim()) { 124 | Ok(cmd) => cmd, 125 | Err(e) => { 126 | tracing::warn!("Invalid message {:?}. {:?}", command, e); 127 | return service.kill(); 128 | } 129 | }; 130 | handle_command(service, cmd, state); 131 | } 132 | 133 | fn handle_command(mut service: Service, cmd: Command, state: Arc>>) { 134 | match cmd { 135 | Command::Ports => { 136 | if let Ok(outputs) = state.lock() { 137 | service.write_response(Response::Ports(outputs.iter().map(Clone::clone).collect())); 138 | } 139 | } 140 | } 141 | } 142 | 143 | fn main() { 144 | tracing_subscriber::fmt::fmt() 145 | .with_env_filter(EnvFilter::from_default_env()) 146 | .init(); 147 | 148 | let executor = tokio::runtime::Builder::new_current_thread() 149 | .enable_all() 150 | .build() 151 | .unwrap(); 152 | 153 | let state = Arc::new(Mutex::new(Vec::new())); 154 | 155 | executor.block_on(async { 156 | let sync = read_outputs(state.clone()); 157 | let handle = service(state); 158 | 159 | tokio::join!(sync, handle); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /crates/amdvold/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "x86_64-unknown-linux-musl" 3 | 4 | [profile.release] 5 | lto = true 6 | panic = "abort" 7 | codegen-units = 1 8 | -------------------------------------------------------------------------------- /crates/amdvold/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amdvold" 3 | version = "1.0.14" 4 | edition = "2018" 5 | description = "AMDGPU fan control service" 6 | license = "MIT OR Apache-2.0" 7 | keywords = ["hardware", "amdgpu"] 8 | categories = ["hardware-support"] 9 | repository = "https://github.com/Eraden/amdgpud" 10 | authors = ['Adrian Woźniak '] 11 | homepage = 'https://github.com/Eraden/amdgpud' 12 | 13 | [package.metadata] 14 | target = "x86_64-unknown-linux-musl" 15 | 16 | [[bin]] 17 | name = 'amdvold' 18 | path = "./src/main.rs" 19 | 20 | [dependencies] 21 | amdgpu = { path = "../amdgpu", version = "1.0.11" } 22 | amdgpu-config = { path = "../amdgpu-config", version = "1.0.10", features = ["voltage"] } 23 | gumdrop = { workspace = true } 24 | serde = { workspace = true, features = ["derive"] } 25 | thiserror = { workspace = true } 26 | toml = { workspace = true } 27 | tracing = { workspace = true } 28 | tracing-subscriber = { workspace = true, features = ["env-filter"] } 29 | 30 | [dev-dependencies] 31 | amdgpu = { path = "../amdgpu", version = "1.0" } 32 | amdgpu-config = { path = "../amdgpu-config", version = "1.0", features = ["voltage"] } 33 | -------------------------------------------------------------------------------- /crates/amdvold/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adrian Woźniak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /crates/amdvold/README.md: -------------------------------------------------------------------------------- 1 | # AMD graphic card voltage manager 2 | 3 | This tool can be used to overclock you AMD graphic card on Linux 4 | 5 | ## Install 6 | 7 | ```bash 8 | cargo install amdvold 9 | ``` 10 | 11 | ## Usage 12 | 13 | Available commands: 14 | 15 | * `setup-info` - prints information how to enable voltage management on Linux (see Requirements) 16 | * `print-states` - prints current card states 17 | * `change-state` - change card voltage states 18 | * `apply-changes` - apply changes 19 | 20 | ## Changing states 21 | 22 | Positional arguments: 23 | * `index` Profile number 24 | * `module` Either memory or engine 25 | * `frequency` New GPU module frequency 26 | * `voltage` New GPU module voltage 27 | 28 | Optional arguments: 29 | * `-a`, `--apply-immediately` Apply changes immediately after change 30 | 31 | Example: 32 | 33 | ```bash 34 | amdvold 1 engine 1450MHz 772mV 35 | ``` 36 | 37 | ## Requirements 38 | 39 | To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following: 40 | 41 | * In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example: 42 | 43 | GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1" 44 | 45 | Easiest way is to modify "/etc/default/grub" and generate new grub config. 46 | 47 | * If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example: 48 | 49 | options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff 50 | 51 | (only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff") -------------------------------------------------------------------------------- /crates/amdvold/assets/enable_voltage_info.txt: -------------------------------------------------------------------------------- 1 | To enable AMD GPU voltage manipulation kernel parameter must be added, please do one of the following: 2 | 3 | * In GRUB add to "GRUB_CMDLINE_LINUX_DEFAULT" following text "amdgpu.ppfeaturemask=0xffffffff", example: 4 | 5 | GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3 cryptdevice=/dev/nvme0n1p3:cryptroot amdgpu.ppfeaturemask=0xffffffff psi=1" 6 | 7 | Easiest way is to modify "/etc/default/grub" and generate new grub config. 8 | 9 | * If you have hooks enabled add in "/etc/modprobe.d/amdgpu.conf" to "options" following text "amdgpu.ppfeaturemask=0xffffffff", example: 10 | 11 | options amdgpu si_support=1 cik_support=1 vm_fragment_size=9 audio=0 dc=0 aspm=0 ppfeaturemask=0xffffffff 12 | 13 | (only "ppfeaturemask=0xffffffff" is required and if you don't have "options amdgpu" you can just add "options amdgpu ppfeaturemask=0xffffffff") -------------------------------------------------------------------------------- /crates/amdvold/src/apply_changes.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::hw_mons; 2 | 3 | use crate::command::VoltageManipulator; 4 | use crate::{Config, VoltageError}; 5 | 6 | #[derive(Debug, gumdrop::Options)] 7 | pub struct ApplyChanges { 8 | help: bool, 9 | } 10 | 11 | pub fn run(_command: ApplyChanges, config: &Config) -> crate::Result<()> { 12 | let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config); 13 | if mons.is_empty() { 14 | return Err(VoltageError::NoAmdGpu); 15 | } 16 | let mon = mons.remove(0); 17 | mon.write_apply()?; 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /crates/amdvold/src/change_state.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::hw_mons; 2 | 3 | use crate::clock_state::{Frequency, Voltage}; 4 | use crate::command::{HardwareModule, VoltageManipulator}; 5 | use crate::{Config, VoltageError}; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum ChangeStateError { 9 | #[error("No profile index was given")] 10 | Index, 11 | #[error("No frequency was given")] 12 | Freq, 13 | #[error("No voltage was given")] 14 | Voltage, 15 | #[error("No AMD GPU module was given (either memory or engine)")] 16 | Module, 17 | } 18 | 19 | #[derive(Debug, gumdrop::Options)] 20 | pub struct ChangeState { 21 | #[options(help = "Help message")] 22 | help: bool, 23 | #[options(help = "Profile number", free)] 24 | index: u16, 25 | #[options(help = "Either memory or engine", free)] 26 | module: Option, 27 | #[options(help = "New GPU module frequency", free)] 28 | frequency: Option, 29 | #[options(help = "New GPU module voltage", free)] 30 | voltage: Option, 31 | #[options(help = "Apply changes immediately after change")] 32 | apply_immediately: bool, 33 | } 34 | 35 | pub fn run(command: ChangeState, config: &Config) -> crate::Result<()> { 36 | let mut mons = VoltageManipulator::wrap_all(hw_mons(false)?, config); 37 | if mons.is_empty() { 38 | return Err(VoltageError::NoAmdGpu); 39 | } 40 | let mon = mons.remove(0); 41 | let ChangeState { 42 | help: _, 43 | index, 44 | module, 45 | frequency, 46 | voltage, 47 | apply_immediately, 48 | } = command; 49 | mon.write_state( 50 | index, 51 | frequency.ok_or(ChangeStateError::Freq)?, 52 | voltage.ok_or(ChangeStateError::Voltage)?, 53 | module.ok_or(ChangeStateError::Module)?, 54 | )?; 55 | if apply_immediately { 56 | mon.write_apply()?; 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /crates/amdvold/src/command.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::hw_mon::HwMon; 2 | 3 | use crate::apply_changes::ApplyChanges; 4 | use crate::change_state::ChangeState; 5 | use crate::clock_state::{ClockState, Frequency, Voltage}; 6 | use crate::print_states::PrintStates; 7 | use crate::setup_info::SetupInfo; 8 | use crate::{Config, VoltageError}; 9 | 10 | #[derive(Debug)] 11 | pub enum HardwareModule { 12 | Engine, 13 | Memory, 14 | } 15 | 16 | impl std::str::FromStr for HardwareModule { 17 | type Err = VoltageError; 18 | 19 | fn from_str(s: &str) -> Result { 20 | match s.to_lowercase().as_str() { 21 | "memory" => Ok(HardwareModule::Memory), 22 | "engine" => Ok(HardwareModule::Engine), 23 | _ => Err(VoltageError::UnknownHardwareModule(s.to_string())), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Debug, gumdrop::Options)] 29 | pub enum VoltageCommand { 30 | SetupInfo(SetupInfo), 31 | PrintStates(PrintStates), 32 | ChangeState(ChangeState), 33 | ApplyChanges(ApplyChanges), 34 | } 35 | 36 | pub struct VoltageManipulator { 37 | hw_mon: HwMon, 38 | } 39 | 40 | impl std::ops::Deref for VoltageManipulator { 41 | type Target = HwMon; 42 | 43 | fn deref(&self) -> &Self::Target { 44 | &self.hw_mon 45 | } 46 | } 47 | 48 | impl std::ops::DerefMut for VoltageManipulator { 49 | fn deref_mut(&mut self) -> &mut Self::Target { 50 | &mut self.hw_mon 51 | } 52 | } 53 | 54 | impl VoltageManipulator { 55 | pub fn wrap(hw_mon: HwMon, _config: &Config) -> Self { 56 | Self { hw_mon } 57 | } 58 | 59 | pub fn wrap_all(mons: Vec, config: &Config) -> Vec { 60 | mons.into_iter() 61 | .map(|mon| Self::wrap(mon, config)) 62 | .collect() 63 | } 64 | 65 | pub fn write_apply(&self) -> crate::Result<()> { 66 | self.device_write("pp_od_clk_voltage", "c")?; 67 | Ok(()) 68 | } 69 | 70 | pub fn write_state( 71 | &self, 72 | state_index: u16, 73 | freq: Frequency, 74 | voltage: Voltage, 75 | module: HardwareModule, 76 | ) -> crate::Result<()> { 77 | self.device_write( 78 | "pp_od_clk_voltage", 79 | format!( 80 | "{module} {state_index} {freq} {voltage}", 81 | state_index = state_index, 82 | freq = freq.to_string(), 83 | voltage = voltage.to_string(), 84 | module = match module { 85 | HardwareModule::Engine => "s", 86 | HardwareModule::Memory => "m", 87 | }, 88 | ), 89 | )?; 90 | Ok(()) 91 | } 92 | 93 | pub fn clock_states(&self) -> crate::Result { 94 | let state = self.device_read("pp_od_clk_voltage")?.parse()?; 95 | Ok(state) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /crates/amdvold/src/error.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::{utils, AmdGpuError}; 2 | use amdgpu_config::voltage::ConfigError; 3 | 4 | use crate::change_state::ChangeStateError; 5 | use crate::clock_state::ClockStateError; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum VoltageError { 9 | #[error("No AMD GPU card was found")] 10 | NoAmdGpu, 11 | #[error("Unknown hardware module {0:?}")] 12 | UnknownHardwareModule(String), 13 | #[error("{0}")] 14 | AmdGpu(AmdGpuError), 15 | #[error("{0}")] 16 | Config(#[from] ConfigError), 17 | #[error("{0:}")] 18 | Io(#[from] std::io::Error), 19 | #[error("{0:}")] 20 | ClockState(#[from] ClockStateError), 21 | #[error("{0:}")] 22 | ChangeStateError(#[from] ChangeStateError), 23 | #[error("{0:}")] 24 | AmdUtils(#[from] utils::AmdGpuError), 25 | } 26 | -------------------------------------------------------------------------------- /crates/amdvold/src/main.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::ensure_config_dir; 2 | use amdgpu_config::voltage::{load_config, Config}; 3 | use gumdrop::Options; 4 | use tracing::error; 5 | use tracing::level_filters::LevelFilter; 6 | use tracing_subscriber::EnvFilter; 7 | 8 | use crate::command::VoltageCommand; 9 | use crate::error::VoltageError; 10 | 11 | mod apply_changes; 12 | mod change_state; 13 | mod clock_state; 14 | mod command; 15 | mod error; 16 | mod print_states; 17 | mod setup_info; 18 | 19 | pub static DEFAULT_CONFIG_PATH: &str = "/etc/amdfand/voltage.toml"; 20 | 21 | pub type Result = std::result::Result; 22 | 23 | #[derive(gumdrop::Options)] 24 | pub struct Opts { 25 | #[options(help = "Help message")] 26 | help: bool, 27 | #[options(help = "Print version")] 28 | version: bool, 29 | #[options(help = "Config location")] 30 | config: Option, 31 | #[options(command)] 32 | command: Option, 33 | } 34 | 35 | fn run(config: Config) -> Result<()> { 36 | let opts: Opts = Opts::parse_args_default_or_exit(); 37 | 38 | if opts.version { 39 | println!("amdfand {}", env!("CARGO_PKG_VERSION")); 40 | std::process::exit(0); 41 | } 42 | 43 | match opts.command { 44 | None => { 45 | Opts::usage(); 46 | Ok(()) 47 | } 48 | Some(VoltageCommand::PrintStates(command)) => print_states::run(command, config), 49 | Some(VoltageCommand::SetupInfo(command)) => setup_info::run(command, &config), 50 | Some(VoltageCommand::ChangeState(command)) => change_state::run(command, &config), 51 | Some(VoltageCommand::ApplyChanges(command)) => apply_changes::run(command, &config), 52 | } 53 | } 54 | 55 | fn setup() -> Result<(String, Config)> { 56 | if std::env::var("RUST_LOG").is_err() { 57 | std::env::set_var("RUST_LOG", "DEBUG"); 58 | } 59 | ensure_config_dir()?; 60 | 61 | let config_path = Opts::parse_args_default_or_exit() 62 | .config 63 | .unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string()); 64 | let config = load_config(&config_path)?; 65 | tracing_subscriber::fmt::fmt() 66 | .with_env_filter(EnvFilter::from_default_env()) 67 | .with_max_level(config.log_level().as_str().parse::().unwrap()) 68 | .init(); 69 | Ok((config_path, config)) 70 | } 71 | 72 | fn main() -> Result<()> { 73 | let (config_path, config) = match setup() { 74 | Ok(config) => config, 75 | Err(e) => { 76 | error!("{}", e); 77 | std::process::exit(1); 78 | } 79 | }; 80 | match run(config) { 81 | Ok(()) => Ok(()), 82 | Err(e) => { 83 | let _config = load_config(&config_path).expect( 84 | "Unable to restore automatic voltage control due to unreadable config file", 85 | ); 86 | // panic_handler::restore_automatic(config); 87 | error!("{}", e); 88 | std::process::exit(1); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/amdvold/src/print_states.rs: -------------------------------------------------------------------------------- 1 | use amdgpu::utils::hw_mons; 2 | 3 | use crate::command::VoltageManipulator; 4 | use crate::Config; 5 | 6 | #[derive(Debug, gumdrop::Options)] 7 | pub struct PrintStates { 8 | help: bool, 9 | } 10 | 11 | pub fn run(_command: PrintStates, config: Config) -> crate::Result<()> { 12 | let mons = VoltageManipulator::wrap_all(hw_mons(false)?, &config); 13 | for mon in mons { 14 | let states = mon.clock_states()?; 15 | println!("Engine clock frequencies:"); 16 | if let Some(freq) = states.engine_label_lowest { 17 | println!(" LOWEST {}", freq.to_string()); 18 | } 19 | if let Some(freq) = states.engine_label_highest { 20 | println!(" HIGHEST {}", freq.to_string()); 21 | } 22 | println!(); 23 | println!("Memory clock frequencies:"); 24 | if let Some(freq) = states.memory_label_lowest { 25 | println!(" LOWEST {}", freq.to_string()); 26 | } 27 | if let Some(freq) = states.memory_label_highest { 28 | println!(" HIGHEST {}", freq.to_string()); 29 | } 30 | println!(); 31 | println!("Curves:"); 32 | for curve in states.curve_labels.iter() { 33 | println!( 34 | " {:>10} {:>10}", 35 | curve.freq.to_string(), 36 | curve.voltage.to_string() 37 | ); 38 | } 39 | println!(); 40 | } 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /crates/amdvold/src/setup_info.rs: -------------------------------------------------------------------------------- 1 | use amdgpu_config::voltage::Config; 2 | 3 | pub static ENABLE_VOLTAGE_INFO: &str = include_str!("../assets/enable_voltage_info.txt"); 4 | 5 | #[derive(Debug, gumdrop::Options)] 6 | pub struct SetupInfo { 7 | help: bool, 8 | } 9 | 10 | pub fn run(_command: SetupInfo, _config: &Config) -> crate::Result<()> { 11 | println!("{}", ENABLE_VOLTAGE_INFO); 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /examples/cards_config.toml: -------------------------------------------------------------------------------- 1 | log_level = "Info" 2 | cards = ["card0"] 3 | 4 | [[temp_matrix]] 5 | temp = 4.0 6 | speed = 4.0 7 | 8 | [[temp_matrix]] 9 | temp = 30.0 10 | speed = 33.0 11 | 12 | [[temp_matrix]] 13 | temp = 45.0 14 | speed = 50.0 15 | 16 | [[temp_matrix]] 17 | temp = 60.0 18 | speed = 66.0 19 | 20 | [[temp_matrix]] 21 | temp = 65.0 22 | speed = 69.0 23 | 24 | [[temp_matrix]] 25 | temp = 70.0 26 | speed = 75.0 27 | 28 | [[temp_matrix]] 29 | temp = 75.0 30 | speed = 89.0 31 | 32 | [[temp_matrix]] 33 | temp = 80.0 34 | speed = 100.0 35 | -------------------------------------------------------------------------------- /examples/default_config.toml: -------------------------------------------------------------------------------- 1 | log_level = "Error" 2 | temp_input = "temp1_input" 3 | 4 | [[temp_matrix]] 5 | temp = 4.0 6 | speed = 4.0 7 | 8 | [[temp_matrix]] 9 | temp = 30.0 10 | speed = 33.0 11 | 12 | [[temp_matrix]] 13 | temp = 45.0 14 | speed = 50.0 15 | 16 | [[temp_matrix]] 17 | temp = 60.0 18 | speed = 66.0 19 | 20 | [[temp_matrix]] 21 | temp = 65.0 22 | speed = 69.0 23 | 24 | [[temp_matrix]] 25 | temp = 70.0 26 | speed = 75.0 27 | 28 | [[temp_matrix]] 29 | temp = 75.0 30 | speed = 89.0 31 | 32 | [[temp_matrix]] 33 | temp = 80.0 34 | speed = 100.0 35 | -------------------------------------------------------------------------------- /examples/unsorted_speed_config.toml: -------------------------------------------------------------------------------- 1 | log_level = "Error" 2 | 3 | [[temp_matrix]] 4 | temp = 4.0 5 | speed = 4.0 6 | 7 | [[temp_matrix]] 8 | temp = 30.0 9 | speed = 33.0 10 | 11 | [[temp_matrix]] 12 | temp = 45.0 13 | speed = 50.0 14 | 15 | [[temp_matrix]] 16 | temp = 60.0 17 | speed = 66.0 18 | 19 | [[temp_matrix]] 20 | temp = 65.0 21 | speed = 69.0 22 | 23 | [[temp_matrix]] 24 | temp = 70.0 25 | speed = 60.0 26 | 27 | [[temp_matrix]] 28 | temp = 75.0 29 | speed = 89.0 30 | 31 | [[temp_matrix]] 32 | temp = 80.0 33 | speed = 100.0 34 | -------------------------------------------------------------------------------- /examples/unsorted_temp_config.toml: -------------------------------------------------------------------------------- 1 | log_level = "Error" 2 | 3 | [[temp_matrix]] 4 | temp = 4.0 5 | speed = 4.0 6 | 7 | [[temp_matrix]] 8 | temp = 30.0 9 | speed = 33.0 10 | 11 | [[temp_matrix]] 12 | temp = 45.0 13 | speed = 50.0 14 | 15 | [[temp_matrix]] 16 | temp = 60.0 17 | speed = 66.0 18 | 19 | [[temp_matrix]] 20 | temp = 65.0 21 | speed = 69.0 22 | 23 | [[temp_matrix]] 24 | temp = 70.0 25 | speed = 75.0 26 | 27 | [[temp_matrix]] 28 | temp = 95.0 29 | speed = 89.0 30 | 31 | [[temp_matrix]] 32 | temp = 80.0 33 | speed = 100.0 34 | -------------------------------------------------------------------------------- /examples/verbose_config.toml: -------------------------------------------------------------------------------- 1 | log_level = "Trace" 2 | temp_input = "temp1_input" 3 | update_rate = 100 4 | 5 | [[temp_matrix]] 6 | temp = 4.0 7 | speed = 4.0 8 | 9 | [[temp_matrix]] 10 | temp = 30.0 11 | speed = 33.0 12 | 13 | [[temp_matrix]] 14 | temp = 45.0 15 | speed = 50.0 16 | 17 | [[temp_matrix]] 18 | temp = 60.0 19 | speed = 66.0 20 | 21 | [[temp_matrix]] 22 | temp = 65.0 23 | speed = 69.0 24 | 25 | [[temp_matrix]] 26 | temp = 70.0 27 | speed = 75.0 28 | 29 | [[temp_matrix]] 30 | temp = 75.0 31 | speed = 89.0 32 | 33 | [[temp_matrix]] 34 | temp = 80.0 35 | speed = 100.0 36 | 37 | [[usage_matrix]] 38 | usage = 30.0 39 | speed = 34.0 40 | 41 | [[usage_matrix]] 42 | usage = 600.0 43 | speed = 50.0 44 | 45 | [[usage_matrix]] 46 | usage = 85.0 47 | speed = 60.0 48 | 49 | [[usage_matrix]] 50 | usage = 100.0 51 | speed = 100.0 52 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | group_imports = "StdExternalCrate" 3 | reorder_modules = true 4 | reorder_imports = true 5 | use_field_init_shorthand = true 6 | wrap_comments = true 7 | edition = "2021" 8 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | rustup default nightly 4 | 5 | set -e +x 6 | 7 | ROOT="$(git rev-parse --show-toplevel)" 8 | 9 | cd ${ROOT} 10 | 11 | rm -Rf ${ROOT}/tmp 12 | mkdir -p ${ROOT}/tmp 13 | 14 | ./scripts/compile.sh 15 | 16 | strip target/x86_64-unknown-linux-musl/release/amdfand 17 | strip target/x86_64-unknown-linux-musl/release/amdvold 18 | strip target/x86_64-unknown-linux-musl/release/amdmond 19 | 20 | function build() { 21 | zip_name=$1 22 | 23 | cd ${ROOT} 24 | 25 | strip target/x86_64-unknown-linux-gnu/release/$zip_name 26 | cp ./target/x86_64-unknown-linux-gnu/release/$zip_name ./tmp/amdguid 27 | 28 | cd ${ROOT}/tmp 29 | zip ${zip_name}.zip ./amdguid 30 | cd ${ROOT} 31 | } 32 | 33 | build agc 34 | -------------------------------------------------------------------------------- /scripts/ci/build-daemon-pkg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PATH=$PATH:$HOME/.cargo/bin 4 | 5 | IMAGE=$1 6 | VERSION=$2 7 | APP_NAME=$3 8 | 9 | # APP_VERSION=$(grep -m 1 -E '^version = ' crates/${APP_NAME}/Cargo.toml | sed 's/version = //' | sed 's/\"//g') 10 | APP_VERSION=$(jq -M -c -r "map(select(.name == \"${APP_NAME}\")) | .[0].version" ./metadata.json) 11 | 12 | zip build/${APP_NAME}-${IMAGE}-${VERSION}-${APP_VERSION}.zip \ 13 | target/release/${APP_NAME} \ 14 | services/${APP_NAME}.service 15 | -------------------------------------------------------------------------------- /scripts/ci/build-gui-pkg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | export PATH=$PATH:$HOME/.cargo/bin 6 | 7 | IMAGE=$1 8 | VERSION=$2 9 | APP_NAME=$3 10 | 11 | # APP_VERSION=$(grep -m 1 -E '^version = ' crates/${APP_NAME}/Cargo.toml | sed 's/version = //' | sed 's/\"//g') 12 | APP_VERSION=$(jq -M -c -r "map(select(.name == \"${APP_NAME}\")) | .[0].version" ./metadata.json) 13 | 14 | zip build/${APP_NAME}-${IMAGE}-${VERSION}-${APP_VERSION}.zip \ 15 | target/release/${APP_NAME} \ 16 | target/release/amdgui-helper \ 17 | services/amdgui-helper.service 18 | -------------------------------------------------------------------------------- /scripts/ci/cargo-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | OS=$1 6 | VER=$2 7 | 8 | export PATH=$PATH:$HOME/.cargo/bin 9 | 10 | rustup default nightly 11 | rustup target install x86_64-unknown-linux-musl 12 | rustup update 13 | 14 | # export SCCACHE_ENDPOINT=https://minio.ita-prog.pl:443 15 | # export SCCACHE_BUCKET=${OS}-cache 16 | # export RUSTC_WRAPPER=$(which sccache) 17 | # export AWS_PROFILE=minio 18 | unset RUSTC_WRAPPER 19 | 20 | cargo metadata --format-version 1 --no-deps | jq -S -M -c ".packages | map({ version: .version, name: .name })" > metadata.json 21 | 22 | cargo build --release --bin amdmond 23 | cargo build --release --bin amdfand 24 | cargo build --release --bin amdports 25 | cargo build --release --bin amdgui-helper 26 | 27 | if command -v cargo-chef 28 | then 29 | cargo chef cook --release --recipe-path recipe.json --bin agc 30 | cargo chef cook --release --recipe-path recipe.json --bin amdvold 31 | else 32 | cargo build --release --bin agc 33 | cargo build --release --bin amdvold 34 | fi 35 | 36 | ls -al 37 | ls -al target 38 | -------------------------------------------------------------------------------- /scripts/ci/cargo-test.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | PATH=$PATH:$HOME/.cargo/bin 4 | rustup default nightly 5 | rustup update 6 | rustup component add rustfmt 7 | rustup target install x86_64-unknown-linux-musl 8 | cargo fmt -- --check 9 | cargo test --release 10 | 11 | for p in $(ls crates); 12 | do 13 | if [[ "$p" != "amdgpu" && "$p" != "amdgui" ]]; then 14 | echo "Testing $p" 15 | rm -Rf target 16 | cargo test --release -p $p 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /scripts/ci/install-archlinux-dependencies.sh: -------------------------------------------------------------------------------- 1 | pacman --noconfirm -Sy rustup clang gcc make cmake git python rust-musl musl libxcb xcb-util shaderc curl cargo-binstall zip jq 2 | rustup default nightly 3 | cargo binstall -y cargo-chef 4 | -------------------------------------------------------------------------------- /scripts/ci/install-ubuntu-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime 4 | 5 | apt-get update 6 | DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata 7 | dpkg-reconfigure --frontend noninteractive tzdata 8 | apt-get install --yes curl gnupg clang gcc cmake build-essential git python3 zip tar wget jq 9 | cp /usr/bin/clang /usr/bin/cc 10 | cp /usr/bin/python3 /usr/bin/python 11 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/install-rustup 12 | chmod +x /tmp/install-rustup 13 | /tmp/install-rustup -y 14 | 15 | export PATH=$PATH:~/.cargo/bin 16 | 17 | wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null 18 | if [[ "${VERSION}" == "18" ]]; then 19 | echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null || echo 1 20 | fi 21 | 22 | apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 42D5A192B819C5DA || echo 0 23 | apt-get update || echo 0 24 | apt-get install --yes upx-ucl xcb libxcb-shape0 libxcb-xfixes0 libxcb-record0 libxcb-shape0-dev libxcb-xfixes0-dev libxcb-record0-dev || echo 0 25 | -------------------------------------------------------------------------------- /scripts/compile.sh: -------------------------------------------------------------------------------- 1 | set -e +x 2 | 3 | rustup default nightly 4 | 5 | cd "$(git rev-parse --show-toplevel)" 6 | 7 | cargo build --release --target x86_64-unknown-linux-musl --bin amdfand 8 | cargo build --release --target x86_64-unknown-linux-musl --bin amdmond 9 | cargo build --release --target x86_64-unknown-linux-musl --bin amdvold 10 | cargo build --release --target x86_64-unknown-linux-musl --bin amdgui-helper 11 | cargo build --release --target x86_64-unknown-linux-gnu --bin agc 12 | -------------------------------------------------------------------------------- /scripts/local_install.sh: -------------------------------------------------------------------------------- 1 | ./scripts/build.sh 2 | 3 | sudo cp ./target/x86_64-unknown-linux-musl/release/amdfand /usr/bin 4 | sudo cp ./target/x86_64-unknown-linux-musl/release/amdmond /usr/bin 5 | sudo cp ./target/x86_64-unknown-linux-musl/release/amdgui-helper /usr/bin 6 | sudo cp ./target/x86_64-unknown-linux-gnu/release/agc /usr/bin 7 | 8 | sudo cp services/amdfand.service /usr/lib/systemd/system/amdfand.service 9 | sudo cp services/amdmond.service /usr/lib/systemd/system/amdmond.service 10 | sudo cp services/amdgui-helper.service /usr/lib/systemd/system/amdgui-helper.service 11 | 12 | sudo systemctl enable --now amdgui-helper 13 | sudo systemctl enable --now amdfand 14 | sudo systemctl enable --now amdmond 15 | -------------------------------------------------------------------------------- /scripts/make-aur.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -x 4 | 5 | ROOT=$(pwd) 6 | AUR_TARGET=${ROOT}/target/aur 7 | 8 | mkdir -p ${AUR_TARGET} 9 | 10 | function resolveTarget { 11 | IF [[ "$1" == '' ]]; THEN 12 | echo "x86_64-unknown-linux-gnu" 13 | ELSE 14 | echo "x86_64-unknown-linux-musl" 15 | FI 16 | } 17 | 18 | function handleApplication { 19 | f=$1 20 | C=$2 21 | cd ${ROOT}/crates/${f} 22 | 23 | cargo build --release 24 | 25 | MUSL_CHECK=$(echo "$C" | grep "target = \"x86_64-unknown-linux-musl\"") 26 | TARGET="$(resolveTarget $MUSL_CHECK)" 27 | 28 | echo TARGET $TARGET 29 | 30 | mkdir -p target/${TARGET}/release 31 | cp ${ROOT}/target/${TARGET}/release/$f ./target/${TARGET}/release/$f 32 | cp ${ROOT}/target/${TARGET}/release/$f ./target/release/$f 33 | 34 | if [[ "${MUSL_CHECK}" == "" ]]; then 35 | # not MUSL 36 | cargo aur 37 | else 38 | # MUSL 39 | cargo aur --musl 40 | fi 41 | mkdir -p ${AUR_TARGET}/${f} 42 | cp *tar.gz ${AUR_TARGET} 43 | cp PKGBUILD ${AUR_TARGET}/${f} 44 | 45 | cd ${ROOT} 46 | } 47 | 48 | for f in $(ls crates); 49 | do 50 | C=$(cat crates/$f/Cargo.toml) 51 | 52 | R=$(echo "$C" | grep -E "path = \"./src/main.rs\"") 53 | 54 | if [[ "$R" != "" ]]; then 55 | handleApplication $f $C 56 | fi 57 | done 58 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | git_root() { echo "$(git rev-parse --show-toplevel)" } 4 | 5 | cd $(git_root)/amdgpu 6 | cargo publish 7 | 8 | cd $(git_root)/amdgpu-config 9 | cargo publish 10 | 11 | cd $(git_root)/amdfand 12 | cargo publish 13 | 14 | cd $(git_root)/amdvold 15 | cargo publish 16 | 17 | cd $(git_root)/amdmond-lib 18 | cargo publish 19 | 20 | cd $(git_root)/amdmond 21 | cargo publish 22 | 23 | cd $(git_root)/amdgui-helper 24 | cargo publish 25 | 26 | 27 | cd $(git_root)/amdguid 28 | cargo publish 29 | -------------------------------------------------------------------------------- /scripts/zip-ci.sh: -------------------------------------------------------------------------------- 1 | echo Building binaries for $1 2 | 3 | cd "$(git rev-parse --show-toplevel)" 4 | 5 | ROOT="$(git rev-parse --show-toplevel)" 6 | 7 | ./scripts/build.sh 8 | 9 | function build_tag_gz() { 10 | zip_name=$1 11 | cd ${ROOT} 12 | for name in $*; do 13 | cp ${ROOT}/target/x86_64-unknown-linux-musl/release/${name} ${ROOT}/tmp 14 | cp ${ROOT}/services/${name}.service ./tmp 15 | 16 | cd ${ROOT}/tmp 17 | tar -cvf ${zip_name}.tar.gz ${name}.service ${name} 18 | cd ${ROOT} 19 | done 20 | 21 | cd ${ROOT}/tmp 22 | for name in $*; do 23 | rm ${name}.service ${name} 24 | done 25 | cd ${ROOT} 26 | } 27 | 28 | function tar_gui() { 29 | tar_name=$1 30 | 31 | cd ${ROOT}/tmp 32 | unzip ${tar_name}.zip 33 | 34 | cp ${ROOT}/target/x86_64-unknown-linux-musl/release/amdgui-helper ${ROOT}/tmp 35 | cp ${ROOT}/services/amdgui-helper.service ${ROOT}/tmp 36 | tar -cvf ${tar_name}.tar.gz amdgui-helper amdguid amdgui-helper.service 37 | } 38 | 39 | build_tag_gz amdfand 40 | build_tag_gz amdmond 41 | build_tag_gz amdvold 42 | 43 | tar_gui agc 44 | 45 | cd ${ROOT}/tmp 46 | 47 | for f in $(ls *.tar.gz); do 48 | md5sum $f 49 | done 50 | 51 | cd ${ROOT}/tmp 52 | 53 | zip -R ${1}.zip *.tar.gz 54 | 55 | #for file in $(ls *.tar.gz); 56 | #do 57 | # mv $file ${ROOT}/${1}-$file 58 | #done 59 | -------------------------------------------------------------------------------- /scripts/zip-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | version=$1 4 | 5 | if [[ "$version" == "" ]]; 6 | then 7 | echo "VERSION is needed" 8 | exit 1 9 | fi 10 | 11 | set -e +x 12 | 13 | cd "$(git rev-parse --show-toplevel)" 14 | 15 | ROOT="$(git rev-parse --show-toplevel)" 16 | 17 | echo Building archlinux.tar.gz 18 | 19 | ./scripts/build.sh 20 | 21 | build_tag_gz() { 22 | zip_name=$1 23 | cd ${ROOT} 24 | for name in $*; do 25 | cp ${ROOT}/target/x86_64-unknown-linux-musl/release/${name} ${ROOT}/tmp 26 | cp ${ROOT}/services/${name}.service ./tmp 27 | 28 | cd ${ROOT}/tmp 29 | tar -cvf ${zip_name}-${version}.tar.gz ${name}.service ${name} 30 | cd ${ROOT} 31 | done 32 | 33 | cd ${ROOT}/tmp 34 | for name in $*; do 35 | rm ${name}.service ${name} 36 | done 37 | cd ${ROOT} 38 | } 39 | 40 | tar_gui() { 41 | tar_name=$1 42 | 43 | cd ${ROOT}/tmp 44 | unzip ${tar_name}.zip 45 | rm ${tar_name}.zip 46 | 47 | cp ${ROOT}/target/x86_64-unknown-linux-musl/release/amdgui-helper ${ROOT}/tmp 48 | cp ${ROOT}/services/amdgui-helper.service ${ROOT}/tmp 49 | tar -cvf ${tar_name}-${version}.tar.gz amdgui-helper amdguid amdgui-helper.service 50 | 51 | rm amdgui-helper 52 | rm amdgui-helper.service 53 | rm amdguid 54 | } 55 | 56 | build_tag_gz amdfand 57 | build_tag_gz amdmond 58 | build_tag_gz amdvold 59 | 60 | tar_gui agc 61 | 62 | cd ${ROOT}/tmp 63 | 64 | for f in $(ls *.tar.gz); do 65 | md5sum $f 66 | done 67 | 68 | tar -cvf archlinux-${version}.tar.gz *.tar.gz 69 | -------------------------------------------------------------------------------- /services/amdfand: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | description="amdfan controller" 4 | pidfile="/run/${SVCNAME}.pid" 5 | command="/usr/bin/amdfand" 6 | command_args="service" 7 | command_user="root" 8 | command_background=true 9 | 10 | depend() { 11 | need udev 12 | } 13 | 14 | stop_pre() { 15 | rm /var/lib/amdfand/amdfand.pid 16 | } 17 | -------------------------------------------------------------------------------- /services/amdfand.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AMD GPU fan daemon 3 | After=sysinit.target local-fs.target 4 | 5 | [Service] 6 | Restart=on-failure 7 | RestartSec=4 8 | 9 | ExecStart=/usr/bin/amdfand service 10 | ExecStopPost=rm /var/lib/amdfand/amdfand.pid 11 | 12 | Environment=RUST_LOG=ERROR 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /services/amdgui-helper: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | description="amdgui helper service" 4 | pidfile="/run/${SVCNAME}.pid" 5 | command="/usr/bin/amdgui-helper" 6 | command_args="service" 7 | command_user="root" 8 | command_background=true 9 | 10 | depend() { 11 | need udev 12 | } 13 | 14 | stop_pre() { 15 | rm /var/lib/amdgui/helper.pid /var/lib/amdfand/helper.sock 16 | } 17 | -------------------------------------------------------------------------------- /services/amdgui-helper.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AMD GPU gui helper 3 | After=sysinit.target local-fs.target 4 | 5 | [Service] 6 | Restart=on-failure 7 | RestartSec=4 8 | 9 | ExecStart=/usr/bin/amdgui-helper 10 | ExecStopPost=rm /var/lib/amdgui/helper.pid /var/lib/amdfand/helper.sock 11 | 12 | Environment=RUST_LOG=ERROR 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /services/amdmond: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | description="AMD card monitoring tool." 4 | pidfile="/run/${SVCNAME}.pid" 5 | command="/usr/bin/amdmond log-file -s /var/log/amdmon.csv" 6 | command_args="service" 7 | command_user="root" 8 | command_background=true 9 | 10 | depend() { 11 | need udev 12 | } 13 | -------------------------------------------------------------------------------- /services/amdmond.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AMD GPU monitoring tool 3 | After=sysinit.target local-fs.target 4 | 5 | [Service] 6 | Restart=on-failure 7 | RestartSec=4 8 | 9 | ExecStart=/usr/bin/amdmond log-file -s /var/log/amdmon.csv 10 | 11 | Environment=RUST_LOG=ERROR 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /services/amdvold: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | description="amdvol controller" 4 | pidfile="/run/${SVCNAME}.pid" 5 | command="/usr/bin/amdvold" 6 | command_args="service" 7 | command_user="root" 8 | command_background=true 9 | 10 | depend() { 11 | need udev 12 | } 13 | -------------------------------------------------------------------------------- /services/amdvold.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AMD GPU voltage daemon 3 | After=sysinit.target local-fs.target 4 | 5 | [Service] 6 | Restart=on-failure 7 | RestartSec=4 8 | 9 | ExecStart=/usr/bin/amdvold service 10 | 11 | Environment=RUST_LOG=ERROR 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | --------------------------------------------------------------------------------