mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
Release new version! (#84)
- Daemon rewrite with JSON API - GTK4 UI - Voltage control on vega20+ - Custom fan curve points in GUI - pkger packaging
This commit is contained in:
commit
2108864fee
60
.github/workflows/build-packages.yaml
vendored
Normal file
60
.github/workflows/build-packages.yaml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
name: Build packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['v2', 'master']
|
||||
|
||||
jobs:
|
||||
build-packages:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install pkger
|
||||
run: |
|
||||
curl -L -o /tmp/pkger.deb https://github.com/vv9k/pkger/releases/download/0.11.0/pkger-0.11.0-0.amd64.deb
|
||||
sudo apt -y install /tmp/pkger.deb
|
||||
|
||||
- name: Build packages
|
||||
run: pkger -c .pkger.yml build lact
|
||||
|
||||
- name: Copy release files
|
||||
run: |
|
||||
OUT_DIR=$PWD/release-artifacts
|
||||
mkdir -p $OUT_DIR
|
||||
|
||||
pushd pkg/output
|
||||
for DISTRO in $(ls); do
|
||||
cd $DISTRO
|
||||
rm -f *.src.rpm
|
||||
|
||||
for FILE in $(ls); do
|
||||
NAME="${FILE%.*}"
|
||||
EXT="${FILE##*.}"
|
||||
|
||||
OUT_NAME="$OUT_DIR/$NAME.$DISTRO.$EXT"
|
||||
cp $FILE $OUT_NAME
|
||||
done
|
||||
cd ..
|
||||
done
|
||||
popd
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1.12.0
|
||||
with:
|
||||
removeArtifacts: true
|
||||
allowUpdates: true
|
||||
artifactErrorsFailBuild: true
|
||||
artifacts: "release-artifacts/*"
|
||||
body: ${{ github.event.head_commit.message }}
|
||||
prerelease: true
|
||||
name: Test release
|
||||
tag: test-build
|
||||
|
||||
- name: Update test-build tag
|
||||
run: |
|
||||
git tag -f test-build
|
||||
git push -f origin test-build
|
||||
shell: bash
|
||||
|
11
.github/workflows/rust.yml
vendored
11
.github/workflows/rust.yml
vendored
@ -11,22 +11,23 @@ env:
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Update repos
|
||||
run: sudo apt update
|
||||
- name: Install dependencies
|
||||
run: sudo apt install libgtk-3-dev libvulkan-dev
|
||||
run: sudo apt install libgtk-4-dev pkg-config libvulkan-dev
|
||||
- name: Build
|
||||
run: cargo build
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
run: cargo test --verbose --no-default-features
|
||||
|
||||
check-format:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: install rustfmt
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,8 @@
|
||||
/target
|
||||
AppDir/
|
||||
*.glade\~
|
||||
*.AppImage
|
||||
*.AppImage.zsync
|
||||
appimage-build/
|
||||
*.stignore
|
||||
pkg/output
|
||||
|
17
.pkger.yml
Normal file
17
.pkger.yml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
recipes_dir: pkg/recipes
|
||||
output_dir: pkg/output
|
||||
images_dir: pkg/images
|
||||
log_dir: ~
|
||||
runtime_uri: ~
|
||||
gpg_key: ~
|
||||
gpg_name: ~
|
||||
ssh: ~
|
||||
images:
|
||||
- name: debian-12
|
||||
target: deb
|
||||
- name: fedora-37
|
||||
target: rpm
|
||||
- name: ubuntu-2204
|
||||
target: deb
|
||||
custom_simple_images: ~
|
138
.vscode/launch.json
vendored
138
.vscode/launch.json
vendored
@ -1,138 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'daemon'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=daemon"
|
||||
],
|
||||
"filter": {
|
||||
"name": "daemon",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'daemon'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=daemon",
|
||||
"--package=daemon"
|
||||
],
|
||||
"filter": {
|
||||
"name": "daemon",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'daemon'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=daemon",
|
||||
"--package=daemon"
|
||||
],
|
||||
"filter": {
|
||||
"name": "daemon",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'cli'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=cli",
|
||||
"--package=cli"
|
||||
],
|
||||
"filter": {
|
||||
"name": "cli",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'cli'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=cli",
|
||||
"--package=cli"
|
||||
],
|
||||
"filter": {
|
||||
"name": "cli",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'gui'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=gui",
|
||||
"--package=gui"
|
||||
],
|
||||
"filter": {
|
||||
"name": "gui",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'gui'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=gui",
|
||||
"--package=gui"
|
||||
],
|
||||
"filter": {
|
||||
"name": "gui",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
42
API.md
Normal file
42
API.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Description
|
||||
|
||||
The LACT Daemon exposes a JSON API over a unix socket, available on `/var/run/lactd.sock`. You can configure who has access to the socket in `/etc/lact/config.yaml` in the `daemon.admin_groups` field.
|
||||
|
||||
The API expects newline-separated JSON objects, and returns a JSON object for every request.
|
||||
|
||||
The general format of requests looks like:
|
||||
```
|
||||
{"command": "command_name", "args": {}}
|
||||
```
|
||||
Note that the type of `args` depends on the specific request, and may be ommited in some cases.
|
||||
|
||||
The response looks like this:
|
||||
```
|
||||
{"status": "ok|error", "data": {}}
|
||||
```
|
||||
Same as `args` in requests, `data` can be of a different type and may not be present depending on the specific request.
|
||||
|
||||
You can try sending commands to socket interactively with `ncat`:
|
||||
```
|
||||
echo '{"command": "list_devices"}' | ncat -U /run/lactd.sock
|
||||
```
|
||||
Example response:
|
||||
```
|
||||
{"status":"ok","data":[{"id":"1002:687F-1043:0555-0000:0b:00.0","name":"Vega 10 XL/XT [Radeon RX Vega 56/64]"}]}
|
||||
```
|
||||
|
||||
# Commands
|
||||
|
||||
For the full list of available commands and responses, you can look at the source code of the schema: [requests](lact-schema/src/request.rs), [the basic response structure](lact-schema/src/response.rs) and [all possible types](lact-schema/src/lib.rs).
|
||||
|
||||
It should also be fairly easy to figure out the API by trial and error, as the error message are quite verbose:
|
||||
|
||||
```
|
||||
echo '{"command": "test"}' | ncat -U /run/lactd.sock
|
||||
|
||||
{"status":"error","data":"Failed to deserialize request: unknown variant `test`, expected one of `ping`, `list_devices`, `system_info`, `device_info`, `device_stats`, `device_clocks_info`, `set_fan_control`, `set_power_cap`, `set_performance_level`, `set_clocks_value` at line 1 column 18"}
|
||||
```
|
||||
|
||||
# Rust
|
||||
|
||||
If you want to connect to the socket from a Rust program, you can simply import either the `lact-client` or `lact-schema` (if you want to write a custom client) crates from this repository.
|
1828
Cargo.lock
generated
1828
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,2 +1,3 @@
|
||||
[workspace]
|
||||
members = ["daemon", "cli", "gui"]
|
||||
resolver = "2"
|
||||
members = ["lact*"]
|
||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 ilyazzz
|
||||
Copyright (c) 2023 Ilya Zlobintsev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
17
Makefile
Normal file
17
Makefile
Normal file
@ -0,0 +1,17 @@
|
||||
export CARGO_TARGET_DIR ?= ./target
|
||||
DESTDIR ?= /usr/local
|
||||
|
||||
build-release:
|
||||
cargo build --release
|
||||
|
||||
install:
|
||||
install -Dm755 target/release/lact ${DESTDIR}/bin/lact
|
||||
install -Dm755 res/lactd.service ${DESTDIR}/lib/systemd/system/lactd.service
|
||||
install -Dm755 res/io.github.lact-linux.desktop ${DESTDIR}/share/applications/io.github.lact-linux.desktop
|
||||
install -Dm755 res/io.github.lact-linux.png ${DESTDIR}/share/pixmaps/io.github.lact-linux.png
|
||||
|
||||
uninstall:
|
||||
rm ${DESTDIR}/bin/lact
|
||||
rm ${DESTDIR}/lib/systemd/system/lactd.service
|
||||
rm ${DESTDIR}/share/applications/io.github.lact-linux.desktop
|
||||
rm ${DESTDIR}/share/pixmaps/io.github.lact-linux.png
|
100
README.md
100
README.md
@ -1,11 +1,12 @@
|
||||
# Linux AMDGPU Control Application
|
||||
|
||||
<img src="res/io.github.lact-linux.png" alt="icon" width="100"/>
|
||||
|
||||
This application allows you to control your AMD GPU on a Linux system.
|
||||
|
||||
| | | |
|
||||
| GPU info | Overclocking | Fan control |
|
||||
|----------------------------------------------|----------------------------------------------|---------------------------------------------|
|
||||
|||
|
||||
|
||||
|||
|
||||
|
||||
Current features:
|
||||
|
||||
@ -15,25 +16,37 @@ Current features:
|
||||
- Basic overclocking
|
||||
|
||||
Currently missing:
|
||||
- Voltage control on Vega20+ GPUs
|
||||
- Precise clock/voltage curve manipulation (currently can only set the maximum values)
|
||||
- <s>Multi-GPU system support</s> *Should work now*
|
||||
|
||||
# Installation
|
||||
|
||||
- Arch Linux: Install the [AUR Package](https://aur.archlinux.org/packages/lact/) (or the -git version)
|
||||
- Debian/Ubuntu/Pop_OS: Download a .deb from [releases](https://github.com/ilyazzz/LACT/releases/). Warning: it has not been tested heavily
|
||||
- Otherwise, build from source:
|
||||
- Debian/Ubuntu/Derevatives: Download a .deb from [releases](https://github.com/ilya-zlobintsev/LACT/releases/).
|
||||
|
||||
It is only available on Debian 12+ and Ubuntu 22.04+ as older versions don't ship gtk4.
|
||||
- Fedora: an rpm is available in [releases](https://github.com/ilya-zlobintsev/LACT/releases/).
|
||||
- Otherwise, build from source.
|
||||
|
||||
**Why is there no AppImage/Flatpak/other universal format?**
|
||||
See [here](./pkg/README.md).
|
||||
|
||||
# Configuration
|
||||
|
||||
There is a configuration file available in `/etc/lact/config.yaml`. Most of the settings are accessible through the GUI, but some of them may be useful to be edited manually (like `admin_groups` to specify who has access to the daemon)
|
||||
|
||||
# Building from source
|
||||
- Install dependencies:
|
||||
- Ubuntu/Debian: `sudo apt install cargo rustc libvulkan-dev git libgtk-3-dev make`
|
||||
- Fedora: `sudo dnf install git gtk3-devel rust cargo vulkan-headers perl-core`
|
||||
|
||||
- `git clone https://github.com/ilyazzz/LACT && cd LACT`
|
||||
- `./deploy.sh`
|
||||
Dependencies:
|
||||
- rust
|
||||
- gtk4
|
||||
- pkg-config
|
||||
- make
|
||||
- hwdata
|
||||
|
||||
Steps:
|
||||
- `git clone https://github.com/ilya-zlobintsev/LACT && cd LACT`
|
||||
- `make`
|
||||
- `sudo make install`
|
||||
|
||||
# Usage
|
||||
|
||||
@ -41,65 +54,48 @@ Enable and start the service (otherwise you won't be able to change any settings
|
||||
```
|
||||
sudo systemctl enable --now lactd
|
||||
```
|
||||
You can now use the application.
|
||||
You can now use the GUI to change settings and view information.
|
||||
|
||||
# API
|
||||
There is an API available over a unix socket. See [here](API.md) for more information.
|
||||
|
||||
# CLI
|
||||
|
||||
There is also a cli available.
|
||||
|
||||
- Getting basic information:
|
||||
- List system GPUs:
|
||||
|
||||
`lact-cli info`
|
||||
`lact cli list-gpus`
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
GPU Model: Radeon RX 570 Pulse 4GB
|
||||
1002:687F-1043:0555-0000:0b:00.0 (Vega 10 XL/XT [Radeon RX Vega 56/64])
|
||||
```
|
||||
- Getting GPU information:
|
||||
|
||||
`lact cli info`
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
lact cli info
|
||||
GPU Vendor: Advanced Micro Devices, Inc. [AMD/ATI]
|
||||
GPU Model: Vega 10 XL/XT [Radeon RX Vega 56/64]
|
||||
Driver in use: amdgpu
|
||||
VBIOS Version: 113-1E3871U-O4C
|
||||
VRAM Size: 4096
|
||||
Link Speed: 8.0 GT/s PCIe
|
||||
```
|
||||
- Getting current GPU stats:
|
||||
|
||||
`lact-cli metrics`
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
VRAM Usage: 545/4096MiB
|
||||
Temperature: 46°C
|
||||
Fan Speed: 785/3200RPM
|
||||
GPU Clock: 783MHz
|
||||
GPU Voltage: 0.975V
|
||||
VRAM Clock: 1750MHz
|
||||
Power Usage: 38/155W
|
||||
VBIOS version: 115-D050PIL-100
|
||||
Link: LinkInfo { current_width: Some("16"), current_speed: Some("8.0 GT/s PCIe"), max_width: Some("16"), max_speed: Some("8.0 GT/s PCIe") }
|
||||
```
|
||||
|
||||
- Showing the current fan curve:
|
||||
|
||||
`lact-cli curve status`
|
||||
|
||||
Example output:
|
||||
|
||||
```
|
||||
Fan curve:
|
||||
20C°: 0%
|
||||
40C°: 0%
|
||||
60C°: 50%
|
||||
80C°: 88%
|
||||
100C°: 100%
|
||||
```
|
||||
The functionality of the CLI is quite limited. If you want to integrate LACT with some application/script, you should use the [API](API.md) instead.
|
||||
|
||||
# Reporting issues
|
||||
|
||||
When reporting issues, please include your system info and GPU model.
|
||||
When reporting issues, please include your system info and GPU model.
|
||||
|
||||
If there's a crash, run `lact-gui` from the command line to get logs, or use `journalctl -u lactd` to see if the daemon crashed.
|
||||
If there's a crash, run `lact gui` from the command line to get logs, or use `journalctl -u lactd` to see if the daemon crashed.
|
||||
|
||||
If there's an issue with GPU model identification please report it [here](https://github.com/ilyazzz/pci-id-parser/), include your GPU model and the output of `cat /sys/class/drm/card*/device/uevent`.
|
||||
|
||||
# Alternatives
|
||||
|
||||
If LACT doesn't end up working for you, make sure to check out [CoreCtrl](https://gitlab.com/corectrl/corectrl).
|
||||
If LACT doesn't do what you want, make sure to check out [CoreCtrl](https://gitlab.com/corectrl/corectrl).
|
||||
|
@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
authors = ["Ilya Zlobintsev <ilya.zl@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
daemon = { path = "../daemon" }
|
||||
structopt = "0.3"
|
||||
log = "0.4"
|
||||
env_logger = "0.8"
|
||||
colored = "2"
|
236
cli/src/main.rs
236
cli/src/main.rs
@ -1,236 +0,0 @@
|
||||
use colored::*;
|
||||
use daemon::daemon_connection::DaemonConnection;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum ConfigOpt {
|
||||
Show,
|
||||
AllowOnlineUpdating,
|
||||
DisallowOnlineUpdating,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
enum CurveOpt {
|
||||
/// Shows current fan control information
|
||||
Status {
|
||||
/// Specify a GPU ID as printed in `lact-cli gpus`. By default, all GPUs are printed.
|
||||
gpu_id: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "lower")]
|
||||
enum Opt {
|
||||
/// Realtime GPU information
|
||||
Metrics {
|
||||
/// Specify a GPU ID as printed in `lact-cli gpus`. By default, all GPUs are printed.
|
||||
gpu_id: Option<u32>,
|
||||
},
|
||||
/// Get GPU list
|
||||
Gpus,
|
||||
/// General information about the GPU
|
||||
Info {
|
||||
/// Specify a GPU ID as printed in `lact-cli gpus`. By default, all GPUs are printed.
|
||||
gpu_id: Option<u32>,
|
||||
},
|
||||
Config(ConfigOpt),
|
||||
/// Fan curve control
|
||||
Curve(CurveOpt),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let d = DaemonConnection::new().unwrap();
|
||||
log::trace!("connection established");
|
||||
|
||||
match opt {
|
||||
Opt::Gpus => {
|
||||
let gpus = d.get_gpus();
|
||||
println!("{:?}", gpus);
|
||||
}
|
||||
Opt::Metrics { gpu_id } => {
|
||||
let mut gpu_ids: Vec<u32> = Vec::new();
|
||||
|
||||
if let Some(gpu_id) = gpu_id {
|
||||
gpu_ids.push(gpu_id);
|
||||
} else {
|
||||
for (gpu_id, _) in d.get_gpus().unwrap() {
|
||||
gpu_ids.push(gpu_id);
|
||||
}
|
||||
}
|
||||
|
||||
for gpu_id in gpu_ids {
|
||||
print_stats(&d, gpu_id);
|
||||
}
|
||||
}
|
||||
Opt::Info { gpu_id } => {
|
||||
let mut gpu_ids: Vec<u32> = Vec::new();
|
||||
|
||||
if let Some(gpu_id) = gpu_id {
|
||||
gpu_ids.push(gpu_id);
|
||||
} else {
|
||||
for (gpu_id, _) in d.get_gpus().unwrap() {
|
||||
gpu_ids.push(gpu_id);
|
||||
}
|
||||
}
|
||||
|
||||
for gpu_id in gpu_ids {
|
||||
print_info(&d, gpu_id);
|
||||
}
|
||||
}
|
||||
Opt::Curve(curve) => match curve {
|
||||
CurveOpt::Status { gpu_id } => {
|
||||
let mut gpu_ids: Vec<u32> = Vec::new();
|
||||
|
||||
if let Some(gpu_id) = gpu_id {
|
||||
gpu_ids.push(gpu_id);
|
||||
} else {
|
||||
for (gpu_id, _) in d.get_gpus().unwrap() {
|
||||
gpu_ids.push(gpu_id);
|
||||
}
|
||||
}
|
||||
|
||||
for gpu_id in gpu_ids {
|
||||
print_fan_curve(&d, gpu_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
Opt::Config(config_opt) => match config_opt {
|
||||
ConfigOpt::Show => print_config(&d),
|
||||
ConfigOpt::AllowOnlineUpdating => enable_online_update(&d),
|
||||
ConfigOpt::DisallowOnlineUpdating => disable_online_update(&d),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn disable_online_update(d: &DaemonConnection) {
|
||||
let mut config = d.get_config().unwrap();
|
||||
config.allow_online_update = Some(false);
|
||||
d.set_config(config).unwrap();
|
||||
}
|
||||
|
||||
fn enable_online_update(d: &DaemonConnection) {
|
||||
let mut config = d.get_config().unwrap();
|
||||
config.allow_online_update = Some(true);
|
||||
d.set_config(config).unwrap();
|
||||
}
|
||||
|
||||
fn print_config(d: &DaemonConnection) {
|
||||
let config = d.get_config().unwrap();
|
||||
|
||||
println!(
|
||||
"{} {:?}",
|
||||
"Online PCI DB updating:".purple(),
|
||||
config.allow_online_update
|
||||
);
|
||||
}
|
||||
|
||||
fn print_fan_curve(d: &DaemonConnection, gpu_id: u32) {
|
||||
let fan_control = d.get_fan_control(gpu_id).unwrap();
|
||||
|
||||
if fan_control.enabled {
|
||||
println!("{}", "Fan curve:".yellow());
|
||||
|
||||
for (temp, fan_speed) in fan_control.curve {
|
||||
println!(
|
||||
"{}{}: {}{}",
|
||||
temp.to_string().yellow(),
|
||||
"C°".yellow(),
|
||||
fan_speed.round().to_string().bold(),
|
||||
"%".bold()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Automatic fan control used".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn print_info(d: &DaemonConnection, gpu_id: u32) {
|
||||
let gpu_info = d.get_gpu_info(gpu_id).unwrap();
|
||||
println!(
|
||||
"{} {}",
|
||||
"GPU Model:".blue(),
|
||||
gpu_info.vendor_data.card_model.unwrap_or_default().bold()
|
||||
);
|
||||
println!(
|
||||
"{} {}",
|
||||
"GPU Vendor:".blue(),
|
||||
gpu_info.vendor_data.gpu_vendor.unwrap_or_default().bold()
|
||||
);
|
||||
println!("{} {}", "Driver in use:".blue(), gpu_info.driver.bold());
|
||||
println!(
|
||||
"{} {}",
|
||||
"VBIOS Version:".blue(),
|
||||
gpu_info.vbios_version.bold()
|
||||
);
|
||||
println!(
|
||||
"{} {}",
|
||||
"VRAM Size:".blue(),
|
||||
gpu_info.vram_size.to_string().bold()
|
||||
);
|
||||
println!("{} {}", "Link Speed:".blue(), gpu_info.link_speed.bold());
|
||||
}
|
||||
|
||||
fn print_stats(d: &DaemonConnection, gpu_id: u32) {
|
||||
let gpu_stats = d.get_gpu_stats(gpu_id).unwrap();
|
||||
println!(
|
||||
"{} {}/{}{}",
|
||||
"VRAM Usage:".green(),
|
||||
gpu_stats.mem_used.unwrap_or_default().to_string().bold(),
|
||||
gpu_stats.mem_total.unwrap_or_default().to_string().bold(),
|
||||
"MiB".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}{}",
|
||||
"Temperature:".green(),
|
||||
gpu_stats
|
||||
.temperatures
|
||||
.get("edge")
|
||||
.unwrap()
|
||||
.current
|
||||
.to_string()
|
||||
.bold(),
|
||||
"°C".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}/{}{}",
|
||||
"Fan Speed:".green(),
|
||||
gpu_stats.fan_speed.unwrap_or_default().to_string().bold(),
|
||||
gpu_stats
|
||||
.max_fan_speed
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
.bold(),
|
||||
"RPM".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}{}",
|
||||
"GPU Clock:".green(),
|
||||
gpu_stats.gpu_freq.unwrap_or_default().to_string().bold(),
|
||||
"MHz".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}{}",
|
||||
"GPU Voltage:".green(),
|
||||
(gpu_stats.voltage.unwrap_or_default() as f64 / 1000.0)
|
||||
.to_string()
|
||||
.bold(),
|
||||
"V".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}{}",
|
||||
"VRAM Clock:".green(),
|
||||
gpu_stats.mem_freq.unwrap_or_default().to_string().bold(),
|
||||
"MHz".bold(),
|
||||
);
|
||||
println!(
|
||||
"{} {}/{}{}",
|
||||
"Power Usage:".green(),
|
||||
gpu_stats.power_avg.unwrap_or_default().to_string().bold(),
|
||||
gpu_stats.power_cap.unwrap_or_default().to_string().bold(),
|
||||
"W".bold(),
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "daemon"
|
||||
version = "0.1.0"
|
||||
authors = ["Ilya Zlobintsev <ilya.zl@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
vulkano = "0.26"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
rand = "0.8"
|
||||
signal-hook = "0.3"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
nix = "0.23"
|
||||
pciid-parser = { version = "0.6.0", features = ["online"] }
|
@ -1,118 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::gpu_controller::PowerProfile;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
IoError(io::Error),
|
||||
ParseError(serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ConfigError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
ConfigError::IoError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ConfigError {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
ConfigError::ParseError(error)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, Hash, Eq)]
|
||||
pub struct GpuIdentifier {
|
||||
pub pci_id: String,
|
||||
pub card_model: Option<String>,
|
||||
pub gpu_model: Option<String>,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl PartialEq for GpuIdentifier {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.pci_id == other.pci_id
|
||||
&& self.gpu_model == other.gpu_model
|
||||
&& self.card_model == other.card_model
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GpuConfig {
|
||||
pub fan_control_enabled: bool,
|
||||
pub fan_curve: BTreeMap<i64, f64>,
|
||||
pub power_cap: i64,
|
||||
pub power_profile: PowerProfile,
|
||||
pub gpu_max_clock: i64,
|
||||
pub gpu_max_voltage: Option<i64>,
|
||||
pub vram_max_clock: i64,
|
||||
}
|
||||
|
||||
impl GpuConfig {
|
||||
pub fn new() -> Self {
|
||||
let mut fan_curve: BTreeMap<i64, f64> = BTreeMap::new();
|
||||
fan_curve.insert(20, 0f64);
|
||||
fan_curve.insert(40, 0f64);
|
||||
fan_curve.insert(60, 50f64);
|
||||
fan_curve.insert(80, 80f64);
|
||||
fan_curve.insert(100, 100f64);
|
||||
|
||||
GpuConfig {
|
||||
fan_curve,
|
||||
fan_control_enabled: false,
|
||||
power_cap: -1,
|
||||
power_profile: PowerProfile::Auto,
|
||||
gpu_max_clock: 0,
|
||||
gpu_max_voltage: None,
|
||||
vram_max_clock: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub gpu_configs: HashMap<u32, (GpuIdentifier, GpuConfig)>,
|
||||
pub allow_online_update: Option<bool>,
|
||||
pub config_path: PathBuf,
|
||||
pub group: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(config_path: &PathBuf) -> Self {
|
||||
let gpu_configs: HashMap<u32, (GpuIdentifier, GpuConfig)> = HashMap::new();
|
||||
|
||||
Config {
|
||||
gpu_configs,
|
||||
allow_online_update: None,
|
||||
config_path: config_path.clone(),
|
||||
group: String::from("wheel"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_from_file(path: &PathBuf) -> Result<Self, ConfigError> {
|
||||
let json = fs::read_to_string(path)?;
|
||||
|
||||
Ok(serde_json::from_str::<Config>(&json)?)
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), ConfigError> {
|
||||
let json = serde_json::to_string_pretty(self)?;
|
||||
log::info!("saving {}", json.to_string());
|
||||
|
||||
Ok(fs::write(&self.config_path, &json.to_string())?)
|
||||
}
|
||||
}
|
||||
|
||||
/*#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_config() -> Result<(), ConfigError> {
|
||||
let c = Config::new();
|
||||
c.save(PathBuf::from("/tmp/config.json"))
|
||||
}
|
||||
}*/
|
@ -1,228 +0,0 @@
|
||||
use crate::config::Config;
|
||||
use crate::gpu_controller::{FanControlInfo, GpuStats};
|
||||
use crate::gpu_controller::{GpuInfo, PowerProfile};
|
||||
use crate::Daemon;
|
||||
use crate::DaemonError;
|
||||
use crate::{Action, DaemonResponse, SOCK_PATH};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DaemonConnection {}
|
||||
|
||||
pub const BUFFER_SIZE: usize = 4096;
|
||||
|
||||
impl DaemonConnection {
|
||||
pub fn new() -> Result<Self, DaemonError> {
|
||||
let addr = nix::sys::socket::SockAddr::Unix(
|
||||
nix::sys::socket::UnixAddr::new_abstract(SOCK_PATH.as_bytes()).unwrap(),
|
||||
);
|
||||
let socket = nix::sys::socket::socket(
|
||||
nix::sys::socket::AddressFamily::Unix,
|
||||
nix::sys::socket::SockType::Stream,
|
||||
nix::sys::socket::SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
.expect("Creating socket failed");
|
||||
nix::sys::socket::connect(socket, &addr).expect("Socket connect failed");
|
||||
|
||||
nix::unistd::write(socket, &bincode::serialize(&Action::CheckAlive).unwrap())
|
||||
.expect("Writing check alive to socket failed");
|
||||
|
||||
nix::sys::socket::shutdown(socket, nix::sys::socket::Shutdown::Write)
|
||||
.expect("Could not shut down");
|
||||
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
buffer.resize(BUFFER_SIZE, 0);
|
||||
loop {
|
||||
match nix::unistd::read(socket, &mut buffer) {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
assert!(n < buffer.len());
|
||||
if n < buffer.len() {
|
||||
buffer.resize(n, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Error reading from socket: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
nix::sys::socket::shutdown(socket, nix::sys::socket::Shutdown::Both)
|
||||
.expect("Could not shut down");
|
||||
|
||||
nix::unistd::close(socket).expect("Failed to close");
|
||||
|
||||
let result: Result<DaemonResponse, DaemonResponse> =
|
||||
bincode::deserialize(&buffer).expect("failed to deserialize message");
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(DaemonConnection {}),
|
||||
Err(_) => Err(DaemonError::ConnectionFailed),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_action(&self, action: Action) -> Result<DaemonResponse, DaemonError> {
|
||||
let addr = nix::sys::socket::SockAddr::Unix(
|
||||
nix::sys::socket::UnixAddr::new_abstract(SOCK_PATH.as_bytes()).unwrap(),
|
||||
);
|
||||
let socket = nix::sys::socket::socket(
|
||||
nix::sys::socket::AddressFamily::Unix,
|
||||
nix::sys::socket::SockType::Stream,
|
||||
nix::sys::socket::SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
.expect("Socket failed");
|
||||
nix::sys::socket::connect(socket, &addr).expect("connect failed");
|
||||
|
||||
let b = bincode::serialize(&action).unwrap();
|
||||
nix::unistd::write(socket, &b).expect("Writing action to socket failed");
|
||||
|
||||
nix::sys::socket::shutdown(socket, nix::sys::socket::Shutdown::Write)
|
||||
.expect("Could not shut down");
|
||||
|
||||
let buffer = Daemon::read_buffer(socket);
|
||||
|
||||
nix::sys::socket::shutdown(socket, nix::sys::socket::Shutdown::Both)
|
||||
.expect("Failed to shut down");
|
||||
|
||||
nix::unistd::close(socket).expect("Failed to close");
|
||||
|
||||
bincode::deserialize(&buffer).expect("failed to deserialize message")
|
||||
}
|
||||
|
||||
pub fn get_gpu_stats(&self, gpu_id: u32) -> Result<GpuStats, DaemonError> {
|
||||
match self.send_action(Action::GetStats(gpu_id))? {
|
||||
DaemonResponse::GpuStats(stats) => Ok(stats),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_gpu_info(&self, gpu_id: u32) -> Result<GpuInfo, DaemonError> {
|
||||
match self.send_action(Action::GetInfo(gpu_id))? {
|
||||
DaemonResponse::GpuInfo(info) => Ok(info),
|
||||
_ => unreachable!("impossible enum variant"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_fan_control(&self, gpu_id: u32) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::StartFanControl(gpu_id))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => Err(DaemonError::HWMonError),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_fan_control(&self, gpu_id: u32) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::StopFanControl(gpu_id))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => Err(DaemonError::HWMonError),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fan_control(&self, gpu_id: u32) -> Result<FanControlInfo, DaemonError> {
|
||||
match self.send_action(Action::GetFanControl(gpu_id))? {
|
||||
DaemonResponse::FanControlInfo(info) => Ok(info),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_fan_curve(&self, gpu_id: u32, curve: BTreeMap<i64, f64>) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetFanCurve(gpu_id, curve))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_power_cap(&self, gpu_id: u32, cap: i64) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetPowerCap(gpu_id, cap))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_power_profile(&self, gpu_id: u32, profile: PowerProfile) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetPowerProfile(gpu_id, profile))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/*pub fn set_gpu_power_state(&self, gpu_id: u32, num: u32, clockspeed: i64, voltage: Option<i64>) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetGPUPowerState(gpu_id, num, clockspeed, voltage))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}*/
|
||||
|
||||
pub fn set_gpu_max_power_state(
|
||||
&self,
|
||||
gpu_id: u32,
|
||||
clockspeed: i64,
|
||||
voltage: Option<i64>,
|
||||
) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetGPUMaxPowerState(gpu_id, clockspeed, voltage))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_vram_max_clock(&self, gpu_id: u32, clockspeed: i64) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetVRAMMaxClock(gpu_id, clockspeed))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit_gpu_power_states(&self, gpu_id: u32) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::CommitGPUPowerStates(gpu_id))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_gpu_power_states(&self, gpu_id: u32) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::ResetGPUPowerStates(gpu_id))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_gpus(&self) -> Result<HashMap<u32, Option<String>>, DaemonError> {
|
||||
match self.send_action(Action::GetGpus)? {
|
||||
DaemonResponse::Gpus(gpus) => Ok(gpus),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
let addr = nix::sys::socket::SockAddr::Unix(
|
||||
nix::sys::socket::UnixAddr::new_abstract(SOCK_PATH.as_bytes()).unwrap(),
|
||||
);
|
||||
let socket = nix::sys::socket::socket(
|
||||
nix::sys::socket::AddressFamily::Unix,
|
||||
nix::sys::socket::SockType::Stream,
|
||||
nix::sys::socket::SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
.expect("Socket failed");
|
||||
nix::sys::socket::connect(socket, &addr).expect("connect failed");
|
||||
nix::unistd::write(socket, &mut &bincode::serialize(&Action::Shutdown).unwrap())
|
||||
.expect("Writing shutdown to socket failed");
|
||||
}
|
||||
|
||||
pub fn get_config(&self) -> Result<Config, DaemonError> {
|
||||
match self.send_action(Action::GetConfig)? {
|
||||
DaemonResponse::Config(config) => Ok(config),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_config(&self, config: Config) -> Result<(), DaemonError> {
|
||||
match self.send_action(Action::SetConfig(config))? {
|
||||
DaemonResponse::OK => Ok(()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,306 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct Temperature {
|
||||
pub current: i64,
|
||||
pub crit: i64,
|
||||
pub crit_hyst: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum HWMonError {
|
||||
PermissionDenied,
|
||||
InvalidValue,
|
||||
Unsupported,
|
||||
NoHWMon,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct HWMon {
|
||||
hwmon_path: PathBuf,
|
||||
fan_control: Arc<AtomicBool>,
|
||||
fan_curve: Arc<RwLock<BTreeMap<i64, f64>>>,
|
||||
}
|
||||
|
||||
impl HWMon {
|
||||
pub fn new(
|
||||
hwmon_path: &PathBuf,
|
||||
fan_control_enabled: bool,
|
||||
fan_curve: BTreeMap<i64, f64>,
|
||||
power_cap: Option<i64>,
|
||||
) -> HWMon {
|
||||
let mut mon = HWMon {
|
||||
hwmon_path: hwmon_path.clone(),
|
||||
fan_control: Arc::new(AtomicBool::new(false)),
|
||||
fan_curve: Arc::new(RwLock::new(fan_curve)),
|
||||
};
|
||||
|
||||
if fan_control_enabled {
|
||||
mon.start_fan_control().unwrap();
|
||||
}
|
||||
if let Some(cap) = power_cap {
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
mon.set_power_cap(cap);
|
||||
}
|
||||
}
|
||||
|
||||
mon
|
||||
}
|
||||
|
||||
pub fn get_fan_max_speed(&self) -> Option<i64> {
|
||||
match fs::read_to_string(self.hwmon_path.join("fan1_max")) {
|
||||
Ok(speed) => Some(speed.trim().parse().unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fan_speed(&self) -> Option<i64> {
|
||||
/*if self.fan_control.load(Ordering::SeqCst) {
|
||||
let pwm1 = fs::read_to_string(self.hwmon_path.join("pwm1"))
|
||||
.expect("Couldn't read pwm1")
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.unwrap();
|
||||
|
||||
self.fan_max_speed / 255 * pwm1
|
||||
}
|
||||
else {
|
||||
fs::read_to_string(self.hwmon_path.join("fan1_input"))
|
||||
.expect("Couldn't read fan speed")
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.unwrap()
|
||||
}*/
|
||||
match fs::read_to_string(self.hwmon_path.join("fan1_input")) {
|
||||
Ok(a) => Some(a.trim().parse::<i64>().unwrap()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mem_freq(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("freq2_input");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(freq) => Some(freq.trim().parse::<i64>().unwrap() / 1000 / 1000),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_gpu_freq(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("freq1_input");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(freq) => Some(freq.trim().parse::<i64>().unwrap() / 1000 / 1000),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_temps(&self) -> HashMap<String, Temperature> {
|
||||
let mut temps = HashMap::new();
|
||||
|
||||
for i in 1..3 {
|
||||
let label_filename = self.hwmon_path.join(format!("temp{}_label", i));
|
||||
|
||||
match fs::read_to_string(label_filename) {
|
||||
Ok(label) => {
|
||||
// If there's a label identifying the sensor, there should always be input and crit files too. But just in case using .unwrap_or_default()
|
||||
let current = {
|
||||
let filename = self.hwmon_path.join(format!("temp{}_input", i));
|
||||
fs::read_to_string(filename)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.unwrap_or_default()
|
||||
/ 1000
|
||||
};
|
||||
|
||||
let crit = {
|
||||
let filename = self.hwmon_path.join(format!("temp{}_crit", i));
|
||||
fs::read_to_string(filename)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.unwrap_or_default()
|
||||
/ 1000
|
||||
};
|
||||
|
||||
let crit_hyst = {
|
||||
let filename = self.hwmon_path.join(format!("temp{}_crit_hyst", i));
|
||||
fs::read_to_string(filename)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.parse::<i64>()
|
||||
.unwrap_or_default()
|
||||
/ 1000
|
||||
};
|
||||
|
||||
temps.insert(
|
||||
label.trim().to_string(),
|
||||
Temperature {
|
||||
current,
|
||||
crit,
|
||||
crit_hyst,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
temps
|
||||
}
|
||||
|
||||
pub fn get_voltage(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("in0_input");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(voltage) => Some(voltage.trim().parse::<i64>().unwrap()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_cap_max(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("power1_cap_max");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(power_cap) => Some(power_cap.trim().parse::<i64>().unwrap() / 1000000),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_cap(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("power1_cap");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(a) => Some(a.trim().parse::<i64>().unwrap() / 1000000),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_power_cap(&mut self, cap: i64) -> Result<(), HWMonError> {
|
||||
if cap
|
||||
> self
|
||||
.get_power_cap_max()
|
||||
.ok_or_else(|| HWMonError::Unsupported)?
|
||||
{
|
||||
return Err(HWMonError::InvalidValue);
|
||||
}
|
||||
|
||||
let cap = cap * 1000000;
|
||||
log::trace!("setting power cap to {}", cap);
|
||||
|
||||
match fs::write(self.hwmon_path.join("power1_cap"), cap.to_string()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(HWMonError::PermissionDenied),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_avg(&self) -> Option<i64> {
|
||||
let filename = self.hwmon_path.join("power1_average");
|
||||
|
||||
match fs::read_to_string(filename) {
|
||||
Ok(a) => Some(a.trim().parse::<i64>().unwrap() / 1000000),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_fan_curve(&self, curve: BTreeMap<i64, f64>) {
|
||||
log::trace!("trying to set curve");
|
||||
let mut current = self.fan_curve.write().unwrap();
|
||||
current.clear();
|
||||
|
||||
for (k, v) in curve.iter() {
|
||||
current.insert(k.clone(), v.clone());
|
||||
}
|
||||
log::trace!("set curve to {:?}", current);
|
||||
}
|
||||
|
||||
pub fn start_fan_control(&self) -> Result<(), HWMonError> {
|
||||
if self.fan_control.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
self.fan_control.store(true, Ordering::SeqCst);
|
||||
|
||||
match fs::write(self.hwmon_path.join("pwm1_enable"), "1") {
|
||||
Ok(_) => {
|
||||
let s = self.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
while s.fan_control.load(Ordering::SeqCst) {
|
||||
let temps = s.get_temps();
|
||||
log::trace!("Temps: {:?}", temps);
|
||||
|
||||
// Use junction temp when available, otherwise fall back to edge
|
||||
let temps = match temps.get("junction") {
|
||||
Some(temp) => temp,
|
||||
None => temps.get("edge").unwrap(),
|
||||
};
|
||||
|
||||
if temps.current >= temps.crit || temps.current <= temps.crit_hyst {
|
||||
println!("CRITICAL TEMPERATURE DETECTED! FORCING MAX FAN SPEED");
|
||||
fs::write(s.hwmon_path.join("pwm1"), 255.to_string())
|
||||
.expect("Failed to set gpu temp in critical scenario (Warning: GPU Overheating!)");
|
||||
}
|
||||
|
||||
log::trace!("Current gpu temp: {}", temps.current);
|
||||
|
||||
let curve = s.fan_curve.read().unwrap();
|
||||
|
||||
for (t_low, s_low) in curve.iter() {
|
||||
match curve.range(t_low..).nth(1) {
|
||||
Some((t_high, s_high)) => {
|
||||
if (t_low..t_high).contains(&&temps.current) {
|
||||
let speed_ratio = (temps.current - t_low) as f64
|
||||
/ (t_high - t_low) as f64; //The ratio of which speed to choose within the range of current lower and upper speeds
|
||||
let speed_percent =
|
||||
s_low + ((s_high - s_low) * speed_ratio);
|
||||
let pwm = (255f64 * (speed_percent / 100f64)) as i64;
|
||||
log::trace!("pwm: {}", pwm);
|
||||
|
||||
fs::write(s.hwmon_path.join("pwm1"), pwm.to_string())
|
||||
.expect("Failed to write to pwm1");
|
||||
|
||||
log::trace!("In the range of {}..{}c {}..{}%, setting speed {}% ratio {}", t_low, t_high, s_low, s_high, speed_percent, speed_ratio);
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
drop(curve); //needed to release rwlock so that the curve can be changed
|
||||
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(HWMonError::PermissionDenied),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_fan_control(&self) -> Result<(), HWMonError> {
|
||||
match fs::write(self.hwmon_path.join("pwm1_enable"), "2") {
|
||||
Ok(_) => {
|
||||
self.fan_control.store(false, Ordering::SeqCst);
|
||||
log::trace!("Stopping fan control");
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(HWMonError::PermissionDenied),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fan_control(&self) -> (bool, BTreeMap<i64, f64>) {
|
||||
log::trace!("Fan control: {}", self.fan_control.load(Ordering::SeqCst));
|
||||
(
|
||||
self.fan_control.load(Ordering::SeqCst),
|
||||
self.fan_curve.read().unwrap().clone(),
|
||||
)
|
||||
}
|
||||
}
|
@ -1,498 +0,0 @@
|
||||
pub mod config;
|
||||
pub mod daemon_connection;
|
||||
pub mod gpu_controller;
|
||||
pub mod hw_mon;
|
||||
|
||||
use config::{Config, GpuConfig};
|
||||
use gpu_controller::PowerProfile;
|
||||
use pciid_parser::Database;
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fs,
|
||||
};
|
||||
|
||||
use crate::gpu_controller::GpuController;
|
||||
|
||||
// Abstract socket allows anyone to connect without worrying about permissions
|
||||
// https://unix.stackexchange.com/questions/579612/unix-domain-sockets-for-non-root-user
|
||||
pub const SOCK_PATH: &str = "amdgpu-configurator.sock";
|
||||
pub const BUFFER_SIZE: usize = 16384;
|
||||
|
||||
pub struct Daemon {
|
||||
gpu_controllers: HashMap<u32, GpuController>,
|
||||
listener: std::os::unix::io::RawFd,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum Action {
|
||||
CheckAlive,
|
||||
GetConfig,
|
||||
SetConfig(Config),
|
||||
GetGpus,
|
||||
GetInfo(u32),
|
||||
GetStats(u32),
|
||||
StartFanControl(u32),
|
||||
StopFanControl(u32),
|
||||
GetFanControl(u32),
|
||||
SetFanCurve(u32, BTreeMap<i64, f64>),
|
||||
SetPowerCap(u32, i64),
|
||||
SetPowerProfile(u32, PowerProfile),
|
||||
// SetGPUPowerState(u32, u32, i64, Option<i64>),
|
||||
SetGPUMaxPowerState(u32, i64, Option<i64>),
|
||||
SetVRAMMaxClock(u32, i64),
|
||||
CommitGPUPowerStates(u32),
|
||||
ResetGPUPowerStates(u32),
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
pub fn new(unprivileged: bool) -> Daemon {
|
||||
let addr = nix::sys::socket::SockAddr::Unix(
|
||||
nix::sys::socket::UnixAddr::new_abstract(SOCK_PATH.as_bytes()).unwrap(),
|
||||
);
|
||||
let listener = nix::sys::socket::socket(
|
||||
nix::sys::socket::AddressFamily::Unix,
|
||||
nix::sys::socket::SockType::Stream,
|
||||
nix::sys::socket::SockFlag::empty(),
|
||||
None,
|
||||
)
|
||||
.expect("Socket failed");
|
||||
nix::sys::socket::bind(listener, &addr).expect("Bind failed");
|
||||
nix::sys::socket::listen(listener, 128).expect("Listen failed");
|
||||
|
||||
let config_path = PathBuf::from("/etc/lact.json");
|
||||
let mut config = if unprivileged {
|
||||
Config::new(&config_path)
|
||||
} else {
|
||||
match Config::read_from_file(&config_path) {
|
||||
Ok(c) => {
|
||||
log::info!("Loaded config from {}", c.config_path.to_string_lossy());
|
||||
c
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("Config not found, creating");
|
||||
let c = Config::new(&config_path);
|
||||
//c.save().unwrap();
|
||||
c
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("Using config {:?}", config);
|
||||
|
||||
let gpu_controllers = Self::load_gpu_controllers(&mut config);
|
||||
|
||||
if !unprivileged {
|
||||
config.save().unwrap();
|
||||
}
|
||||
|
||||
Daemon {
|
||||
listener,
|
||||
gpu_controllers,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_gpu_controllers(config: &mut Config) -> HashMap<u32, GpuController> {
|
||||
let pci_db = match config.allow_online_update {
|
||||
Some(true) => match Database::get_online() {
|
||||
Ok(db) => Some(db),
|
||||
Err(e) => {
|
||||
log::info!("Error updating PCI db: {:?}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Some(false) | None => None,
|
||||
};
|
||||
|
||||
let mut gpu_controllers: HashMap<u32, GpuController> = HashMap::new();
|
||||
|
||||
'entries: for entry in
|
||||
fs::read_dir("/sys/class/drm").expect("Could not open /sys/class/drm")
|
||||
{
|
||||
let entry = entry.unwrap();
|
||||
if entry.file_name().len() == 5 {
|
||||
if entry.file_name().to_str().unwrap().split_at(4).0 == "card" {
|
||||
log::info!("Initializing {:?}", entry.path());
|
||||
|
||||
let mut controller =
|
||||
GpuController::new(entry.path().join("device"), GpuConfig::new(), &pci_db);
|
||||
|
||||
let current_identifier = controller.get_identifier();
|
||||
|
||||
log::info!(
|
||||
"Searching the config for GPU with identifier {:?}",
|
||||
current_identifier
|
||||
);
|
||||
|
||||
log::info!("{}", &config.gpu_configs.len());
|
||||
for (id, (gpu_identifier, gpu_config)) in &config.gpu_configs {
|
||||
log::info!("Comparing with {:?}", gpu_identifier);
|
||||
if current_identifier == *gpu_identifier {
|
||||
controller.load_config(&gpu_config);
|
||||
gpu_controllers.insert(id.clone(), controller);
|
||||
log::info!("already known");
|
||||
continue 'entries;
|
||||
}
|
||||
|
||||
/*if gpu_info.pci_slot == gpu_identifier.pci_id
|
||||
&& gpu_info.vendor_data.card_model == gpu_identifier.card_model
|
||||
&& gpu_info.vendor_data.gpu_model == gpu_identifier.gpu_model
|
||||
{
|
||||
controller.load_config(&gpu_config);
|
||||
gpu_controllers.insert(id.clone(), controller);
|
||||
log::info!("already known");
|
||||
continue 'entries;
|
||||
}*/
|
||||
}
|
||||
|
||||
log::info!("initializing for the first time");
|
||||
|
||||
let id: u32 = random();
|
||||
|
||||
config
|
||||
.gpu_configs
|
||||
.insert(id, (controller.get_identifier(), controller.get_config()));
|
||||
gpu_controllers.insert(id, controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gpu_controllers
|
||||
}
|
||||
|
||||
pub fn listen(mut self) {
|
||||
loop {
|
||||
let stream = nix::sys::socket::accept(self.listener).expect("Accept failed");
|
||||
if stream < 0 {
|
||||
log::error!("Error from accept");
|
||||
break;
|
||||
} else {
|
||||
Daemon::handle_connection(&mut self, stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_buffer(stream: i32) -> Vec<u8> {
|
||||
log::trace!("Reading buffer");
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
buffer.resize(BUFFER_SIZE, 0);
|
||||
loop {
|
||||
match nix::unistd::read(stream, &mut buffer) {
|
||||
Ok(0) => {
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
assert!(n < buffer.len());
|
||||
if n < buffer.len() {
|
||||
buffer.resize(n, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Error reading from socket: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn handle_connection(&mut self, stream: i32) {
|
||||
let buffer = Self::read_buffer(stream);
|
||||
|
||||
//log::trace!("finished reading, buffer size {}", buffer.len());
|
||||
log::trace!("Attempting to deserialize {:?}", &buffer);
|
||||
//log::trace!("{:?}", action);
|
||||
|
||||
match bincode::deserialize::<Action>(&buffer) {
|
||||
Ok(action) => {
|
||||
log::trace!("Executing action {:?}", action);
|
||||
let response: Result<DaemonResponse, DaemonError> = match action {
|
||||
Action::CheckAlive => Ok(DaemonResponse::OK),
|
||||
Action::GetGpus => {
|
||||
let mut gpus: HashMap<u32, Option<String>> = HashMap::new();
|
||||
for (id, controller) in &self.gpu_controllers {
|
||||
gpus.insert(*id, controller.get_info().vendor_data.gpu_model.clone());
|
||||
}
|
||||
Ok(DaemonResponse::Gpus(gpus))
|
||||
}
|
||||
Action::GetStats(i) => match self.gpu_controllers.get(&i) {
|
||||
Some(controller) => match controller.get_stats() {
|
||||
Ok(stats) => Ok(DaemonResponse::GpuStats(stats)),
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::GetInfo(i) => match self.gpu_controllers.get(&i) {
|
||||
Some(controller) => {
|
||||
Ok(DaemonResponse::GpuInfo(controller.get_info().clone()))
|
||||
}
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::StartFanControl(i) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.start_fan_control() {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::StopFanControl(i) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.stop_fan_control() {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::GetFanControl(i) => match self.gpu_controllers.get(&i) {
|
||||
Some(controller) => match controller.get_fan_control() {
|
||||
Ok(info) => Ok(DaemonResponse::FanControlInfo(info)),
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::SetFanCurve(i, curve) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.set_fan_curve(curve) {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::SetPowerCap(i, cap) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.set_power_cap(cap) {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::HWMonError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::SetPowerProfile(i, profile) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.set_power_profile(profile) {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
/*Action::SetGPUPowerState(i, num, clockspeed, voltage) => {
|
||||
match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => {
|
||||
match controller.set_gpu_power_state(num, clockspeed, voltage) {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
}
|
||||
}
|
||||
None => Err(DaemonError::InvalidID),
|
||||
}
|
||||
}*/
|
||||
Action::SetGPUMaxPowerState(i, clockspeed, voltage) => {
|
||||
match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => {
|
||||
match controller.set_gpu_max_power_state(clockspeed, voltage) {
|
||||
Ok(()) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
}
|
||||
}
|
||||
None => Err(DaemonError::InvalidID),
|
||||
}
|
||||
}
|
||||
Action::SetVRAMMaxClock(i, clockspeed) => {
|
||||
match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => {
|
||||
match controller.set_vram_max_clockspeed(clockspeed) {
|
||||
Ok(()) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
}
|
||||
}
|
||||
None => Err(DaemonError::InvalidID),
|
||||
}
|
||||
}
|
||||
Action::CommitGPUPowerStates(i) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.commit_gpu_power_states() {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::ResetGPUPowerStates(i) => match self.gpu_controllers.get_mut(&i) {
|
||||
Some(controller) => match controller.reset_gpu_power_states() {
|
||||
Ok(_) => {
|
||||
self.config.gpu_configs.insert(
|
||||
i,
|
||||
(controller.get_identifier(), controller.get_config()),
|
||||
);
|
||||
self.config.save().unwrap();
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Err(_) => Err(DaemonError::ControllerError),
|
||||
},
|
||||
None => Err(DaemonError::InvalidID),
|
||||
},
|
||||
Action::Shutdown => {
|
||||
for (id, controller) in &mut self.gpu_controllers {
|
||||
#[allow(unused_must_use)]
|
||||
{
|
||||
controller.reset_gpu_power_states();
|
||||
controller.commit_gpu_power_states();
|
||||
controller.set_power_profile(PowerProfile::Auto);
|
||||
|
||||
if self
|
||||
.config
|
||||
.gpu_configs
|
||||
.get(id)
|
||||
.unwrap()
|
||||
.1
|
||||
.fan_control_enabled
|
||||
{
|
||||
controller.stop_fan_control();
|
||||
}
|
||||
}
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
Action::SetConfig(config) => {
|
||||
self.config = config;
|
||||
self.gpu_controllers.clear();
|
||||
self.gpu_controllers = Self::load_gpu_controllers(&mut self.config);
|
||||
self.config.save().expect("Failed to save config");
|
||||
Ok(DaemonResponse::OK)
|
||||
}
|
||||
Action::GetConfig => Ok(DaemonResponse::Config(self.config.clone())),
|
||||
};
|
||||
|
||||
let buffer = bincode::serialize(&response).unwrap();
|
||||
|
||||
log::trace!("Responding, buffer length {}", buffer.len());
|
||||
nix::unistd::write(stream, &buffer).expect("Writing response to socket failed");
|
||||
|
||||
nix::sys::socket::shutdown(stream, nix::sys::socket::Shutdown::Both)
|
||||
.expect("Failed to shut down");
|
||||
nix::unistd::close(stream).expect("Failed to close");
|
||||
|
||||
log::trace!("Finished responding");
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Failed deserializing action");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DaemonResponse {
|
||||
OK,
|
||||
GpuInfo(gpu_controller::GpuInfo),
|
||||
GpuStats(gpu_controller::GpuStats),
|
||||
Gpus(HashMap<u32, Option<String>>),
|
||||
PowerCap((i64, i64)),
|
||||
FanControlInfo(gpu_controller::FanControlInfo),
|
||||
Config(Config),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum DaemonError {
|
||||
ConnectionFailed,
|
||||
InvalidID,
|
||||
HWMonError,
|
||||
ControllerError,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::gpu_controller::VendorData;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn init() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognize_polaris() {
|
||||
init();
|
||||
|
||||
let db = Database::get_online().unwrap();
|
||||
|
||||
let vendor_data: VendorData = db.get_device_info("1002", "67df", "1da2", "e387").into();
|
||||
|
||||
assert_eq!(
|
||||
vendor_data.gpu_vendor,
|
||||
Some("Advanced Micro Devices, Inc. [AMD/ATI]".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vendor_data.gpu_model,
|
||||
Some("Ellesmere [Radeon RX 470/480/570/570X/580/580X/590]".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vendor_data.card_model,
|
||||
Some("Radeon RX 580 Pulse 4GB".to_string())
|
||||
);
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use std::thread;
|
||||
|
||||
use daemon::{daemon_connection::DaemonConnection, Daemon};
|
||||
use signal_hook::consts::{SIGINT, SIGTERM};
|
||||
use signal_hook::iterator::Signals;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let d = Daemon::new(false);
|
||||
let mut signals = Signals::new(&[SIGTERM, SIGINT]).unwrap();
|
||||
|
||||
thread::spawn(move || {
|
||||
for _ in signals.forever() {
|
||||
log::info!("Shutting down");
|
||||
let d = DaemonConnection::new().unwrap();
|
||||
d.shutdown();
|
||||
}
|
||||
});
|
||||
|
||||
d.listen();
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
Package: lact
|
||||
Version: 0.1
|
||||
Architecture: all
|
||||
Essential: no
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Depends: libgtk-3-0
|
||||
Maintainer: Ilya Zlobintsev
|
||||
Description: AMDGPU Control Utility
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=LACT
|
||||
Description=AMDGPU Control Application
|
||||
Exec=lact-gui
|
11
deploy.sh
11
deploy.sh
@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
cargo build --release &&
|
||||
sudo install -Dm755 target/release/daemon /usr/local/bin/lact-daemon &&
|
||||
sudo install -Dm755 target/release/gui /usr/local/bin/lact-gui &&
|
||||
sudo install -Dm755 target/release/cli /usr/local/bin/lact-cli &&
|
||||
sudo install -Dm644 lactd.service /etc/systemd/system/lactd.service &&
|
||||
sudo mkdir -p /usr/local/share/applications &&
|
||||
sudo install -Dm644 lact.desktop /usr/local/share/applications/ &&
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable lactd &&
|
||||
sudo systemctl restart lactd
|
@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "gui"
|
||||
version = "0.1.0"
|
||||
authors = ["Ilya Zlobintsev <ilya.zl@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
daemon = { path = "../daemon" }
|
||||
|
||||
gtk = { version = "0.14", features = ["v3_22"] }
|
||||
pango = "0.14"
|
||||
glib = "0.14"
|
||||
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
296
gui/src/app.rs
296
gui/src/app.rs
@ -1,296 +0,0 @@
|
||||
mod apply_revealer;
|
||||
mod header;
|
||||
mod root_stack;
|
||||
|
||||
extern crate gtk;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fs,
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
|
||||
use apply_revealer::ApplyRevealer;
|
||||
use daemon::daemon_connection::DaemonConnection;
|
||||
use daemon::gpu_controller::GpuStats;
|
||||
use daemon::DaemonError;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
use header::Header;
|
||||
use root_stack::RootStack;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct App {
|
||||
pub window: Window,
|
||||
pub header: Header,
|
||||
root_stack: RootStack,
|
||||
apply_revealer: ApplyRevealer,
|
||||
daemon_connection: DaemonConnection,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(daemon_connection: DaemonConnection) -> Self {
|
||||
let window = Window::new(WindowType::Toplevel);
|
||||
|
||||
let header = Header::new();
|
||||
|
||||
window.set_titlebar(Some(&header.container));
|
||||
window.set_title("LACT");
|
||||
|
||||
window.set_default_size(500, 600);
|
||||
|
||||
window.connect_delete_event(move |_, _| {
|
||||
main_quit();
|
||||
Inhibit(false)
|
||||
});
|
||||
|
||||
let root_stack = RootStack::new();
|
||||
|
||||
header.set_switcher_stack(&root_stack.container);
|
||||
|
||||
let root_box = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
root_box.add(&root_stack.container);
|
||||
|
||||
let apply_revealer = ApplyRevealer::new();
|
||||
|
||||
root_box.add(&apply_revealer.container);
|
||||
|
||||
window.add(&root_box);
|
||||
|
||||
App {
|
||||
window,
|
||||
header,
|
||||
root_stack,
|
||||
apply_revealer,
|
||||
daemon_connection,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<(), DaemonError> {
|
||||
self.window.show_all();
|
||||
|
||||
let current_gpu_id = Arc::new(AtomicU32::new(0));
|
||||
|
||||
{
|
||||
let current_gpu_id = current_gpu_id.clone();
|
||||
let app = self.clone();
|
||||
|
||||
self.header.connect_gpu_selection_changed(move |gpu_id| {
|
||||
log::info!("GPU Selection changed");
|
||||
app.set_info(gpu_id);
|
||||
current_gpu_id.store(gpu_id, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
let gpus = self.daemon_connection.get_gpus()?;
|
||||
|
||||
self.header.set_gpus(gpus);
|
||||
|
||||
// Show apply button on setting changes
|
||||
{
|
||||
let apply_revealer = self.apply_revealer.clone();
|
||||
|
||||
self.root_stack
|
||||
.thermals_page
|
||||
.connect_settings_changed(move || {
|
||||
log::info!("Settings changed, showing apply button");
|
||||
apply_revealer.show();
|
||||
});
|
||||
|
||||
let apply_revealer = self.apply_revealer.clone();
|
||||
|
||||
self.root_stack.oc_page.connect_settings_changed(move || {
|
||||
log::info!("Settings changed, showing apply button");
|
||||
apply_revealer.show();
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let app = self.clone();
|
||||
let current_gpu_id = current_gpu_id.clone();
|
||||
|
||||
self.root_stack.oc_page.connect_clocks_reset(move || {
|
||||
log::info!("Resetting clocks, but not applying");
|
||||
|
||||
let gpu_id = current_gpu_id.load(Ordering::SeqCst);
|
||||
|
||||
app.daemon_connection
|
||||
.reset_gpu_power_states(gpu_id)
|
||||
.expect("Failed to reset clocks");
|
||||
|
||||
app.set_info(gpu_id);
|
||||
|
||||
app.apply_revealer.show();
|
||||
})
|
||||
}
|
||||
|
||||
// Apply settings
|
||||
{
|
||||
let current_gpu_id = current_gpu_id.clone();
|
||||
let app = self.clone();
|
||||
|
||||
self.apply_revealer.connect_apply_button_clicked(move || {
|
||||
log::info!("Applying settings");
|
||||
|
||||
let gpu_id = current_gpu_id.load(Ordering::SeqCst);
|
||||
|
||||
{
|
||||
let thermals_settings = app.root_stack.thermals_page.get_thermals_settings();
|
||||
|
||||
if thermals_settings.automatic_fan_control_enabled {
|
||||
app.daemon_connection
|
||||
.stop_fan_control(gpu_id)
|
||||
.unwrap_or(println!("Failed to stop fan control"));
|
||||
} else {
|
||||
app.daemon_connection
|
||||
.start_fan_control(gpu_id)
|
||||
.unwrap_or(println!("Failed to start fan control"));
|
||||
}
|
||||
|
||||
app.daemon_connection
|
||||
.set_fan_curve(gpu_id, thermals_settings.curve)
|
||||
.unwrap_or(println!("Failed to set fan curve"));
|
||||
}
|
||||
|
||||
if let Some(clocks_settings) = app.root_stack.oc_page.get_clocks() {
|
||||
app.daemon_connection
|
||||
.set_gpu_max_power_state(
|
||||
gpu_id,
|
||||
clocks_settings.gpu_clock,
|
||||
Some(clocks_settings.gpu_voltage),
|
||||
)
|
||||
.expect("Failed to set GPU clockspeed/voltage");
|
||||
|
||||
app.daemon_connection
|
||||
.set_vram_max_clock(gpu_id, clocks_settings.vram_clock)
|
||||
.expect("Failed to set VRAM Clock");
|
||||
|
||||
app.daemon_connection
|
||||
.commit_gpu_power_states(gpu_id)
|
||||
.expect("Failed to commit power states");
|
||||
}
|
||||
|
||||
if let Some(profile) = app.root_stack.oc_page.get_power_profile() {
|
||||
app.daemon_connection
|
||||
.set_power_profile(gpu_id, profile)
|
||||
.expect("Failed to set power profile");
|
||||
}
|
||||
|
||||
if let Some(cap) = app.root_stack.oc_page.get_power_cap() {
|
||||
app.daemon_connection
|
||||
.set_power_cap(gpu_id, cap)
|
||||
.expect("Failed to set power cap");
|
||||
}
|
||||
|
||||
app.set_info(gpu_id);
|
||||
});
|
||||
}
|
||||
|
||||
self.start_stats_update_loop(current_gpu_id.clone());
|
||||
|
||||
Ok(gtk::main())
|
||||
}
|
||||
|
||||
fn set_info(&self, gpu_id: u32) {
|
||||
let gpu_info = self.daemon_connection.get_gpu_info(gpu_id).unwrap();
|
||||
log::trace!("Setting info {:?}", &gpu_info);
|
||||
|
||||
self.root_stack.info_page.set_info(&gpu_info);
|
||||
|
||||
log::trace!("Setting clocks");
|
||||
self.root_stack.oc_page.set_info(&gpu_info);
|
||||
|
||||
log::trace!("Setting power profile {:?}", gpu_info.power_profile);
|
||||
self.root_stack
|
||||
.oc_page
|
||||
.set_power_profile(&gpu_info.power_profile);
|
||||
|
||||
log::trace!("Setting fan control info");
|
||||
match self.daemon_connection.get_fan_control(gpu_id) {
|
||||
Ok(fan_control_info) => self
|
||||
.root_stack
|
||||
.thermals_page
|
||||
.set_ventilation_info(fan_control_info),
|
||||
Err(_) => self.root_stack.thermals_page.hide_fan_controls(),
|
||||
}
|
||||
|
||||
{
|
||||
// It's overkill to both show and hide the frame, but it needs to be done in set_info because show_all overrides the default hidden state of the frame.
|
||||
match fs::read_to_string("/sys/module/amdgpu/parameters/ppfeaturemask") {
|
||||
Ok(ppfeaturemask) => {
|
||||
const PP_OVERDRIVE_MASK: i32 = 0x4000;
|
||||
|
||||
let ppfeaturemask = ppfeaturemask.trim().strip_prefix("0x").unwrap();
|
||||
|
||||
log::trace!("ppfeaturemask {}", ppfeaturemask);
|
||||
|
||||
let ppfeaturemask: u64 =
|
||||
u64::from_str_radix(ppfeaturemask, 16).expect("Invalid ppfeaturemask");
|
||||
|
||||
if (ppfeaturemask & PP_OVERDRIVE_MASK as u64) > 0 {
|
||||
self.root_stack.oc_page.warning_frame.hide();
|
||||
} else {
|
||||
self.root_stack.oc_page.warning_frame.show();
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::info!("Failed to read feature mask! This is expected if your system doesn't have an AMD GPU.");
|
||||
self.root_stack.oc_page.warning_frame.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.apply_revealer.hide();
|
||||
}
|
||||
|
||||
fn start_stats_update_loop(&self, current_gpu_id: Arc<AtomicU32>) {
|
||||
let context = glib::MainContext::default();
|
||||
|
||||
let _guard = context.acquire();
|
||||
|
||||
// The loop that gets stats
|
||||
let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
{
|
||||
let daemon_connection = self.daemon_connection.clone();
|
||||
|
||||
thread::spawn(move || loop {
|
||||
let gpu_id = current_gpu_id.load(Ordering::SeqCst);
|
||||
|
||||
if let Ok(stats) = daemon_connection.get_gpu_stats(gpu_id) {
|
||||
sender.send(GuiUpdateMsg::GpuStats(stats)).unwrap();
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
});
|
||||
}
|
||||
|
||||
// Receiving stats into the gui event loop
|
||||
{
|
||||
let thermals_page = self.root_stack.thermals_page.clone();
|
||||
let oc_page = self.root_stack.oc_page.clone();
|
||||
|
||||
receiver.attach(None, move |msg| {
|
||||
match msg {
|
||||
GuiUpdateMsg::GpuStats(stats) => {
|
||||
log::trace!("New stats received, updating");
|
||||
thermals_page.set_thermals_info(&stats);
|
||||
oc_page.set_stats(&stats);
|
||||
} /*GuiUpdateMsg::FanControlInfo(fan_control_info) => {
|
||||
thermals_page.set_ventilation_info(fan_control_info)
|
||||
}*/
|
||||
}
|
||||
|
||||
glib::Continue(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GuiUpdateMsg {
|
||||
// FanControlInfo(FanControlInfo),
|
||||
GpuStats(GpuStats),
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApplyRevealer {
|
||||
pub container: Revealer,
|
||||
apply_button: Button,
|
||||
}
|
||||
|
||||
impl ApplyRevealer {
|
||||
pub fn new() -> Self {
|
||||
let container = Revealer::new();
|
||||
|
||||
container.set_transition_duration(150);
|
||||
|
||||
let apply_button = Button::new();
|
||||
|
||||
apply_button.set_label("Apply");
|
||||
|
||||
container.add(&apply_button);
|
||||
|
||||
Self {
|
||||
container,
|
||||
apply_button,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_reveal_child(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_reveal_child(false);
|
||||
}
|
||||
|
||||
pub fn connect_apply_button_clicked<F: Fn() + 'static>(&self, f: F) {
|
||||
self.apply_button.connect_clicked(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
mod info_page;
|
||||
mod oc_page;
|
||||
mod software_page;
|
||||
mod thermals_page;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
use info_page::InformationPage;
|
||||
use oc_page::OcPage;
|
||||
use software_page::SoftwarePage;
|
||||
use thermals_page::ThermalsPage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RootStack {
|
||||
pub container: Stack,
|
||||
pub info_page: InformationPage,
|
||||
pub thermals_page: ThermalsPage,
|
||||
pub software_page: SoftwarePage,
|
||||
pub oc_page: OcPage,
|
||||
}
|
||||
|
||||
impl RootStack {
|
||||
pub fn new() -> Self {
|
||||
let container = Stack::new();
|
||||
|
||||
let info_page = InformationPage::new();
|
||||
|
||||
container.add_titled(&info_page.container, "info_page", "Information");
|
||||
|
||||
let oc_page = OcPage::new();
|
||||
|
||||
container.add_titled(&oc_page.container, "oc_page", "OC");
|
||||
|
||||
let thermals_page = ThermalsPage::new();
|
||||
|
||||
container.add_titled(&thermals_page.container, "thermals_page", "Thermals");
|
||||
|
||||
let software_page = SoftwarePage::new();
|
||||
|
||||
container.add_titled(&software_page.container, "software_page", "Software");
|
||||
|
||||
Self {
|
||||
container,
|
||||
info_page,
|
||||
thermals_page,
|
||||
oc_page,
|
||||
software_page,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
use daemon::gpu_controller::VulkanInfo;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VulkanInfoFrame {
|
||||
pub container: Frame,
|
||||
device_name_label: Label,
|
||||
version_label: Label,
|
||||
features_box: Box,
|
||||
}
|
||||
|
||||
impl VulkanInfoFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(None);
|
||||
|
||||
container.set_label_widget(Some(&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<span font_desc='11'><b>Vulkan Information</b></span>");
|
||||
label
|
||||
}));
|
||||
container.set_label_align(0.5, 0.5);
|
||||
|
||||
container.set_shadow_type(ShadowType::None);
|
||||
|
||||
let grid = Grid::new();
|
||||
|
||||
grid.set_margin_start(5);
|
||||
grid.set_margin_end(5);
|
||||
grid.set_margin_bottom(5);
|
||||
grid.set_margin_top(5);
|
||||
|
||||
grid.set_column_homogeneous(true);
|
||||
|
||||
grid.set_row_spacing(7);
|
||||
grid.set_column_spacing(5);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Device name:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
|
||||
let device_name_label = Label::new(None);
|
||||
device_name_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&device_name_label, 2, 0, 3, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Version:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
|
||||
let version_label = Label::new(None);
|
||||
version_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&version_label, 2, 1, 3, 1);
|
||||
|
||||
let features_expander = Expander::new(Some("Feature support"));
|
||||
|
||||
grid.attach(&features_expander, 0, 2, 5, 1);
|
||||
|
||||
let features_scrolled_window = ScrolledWindow::new(NONE_ADJUSTMENT, NONE_ADJUSTMENT);
|
||||
|
||||
features_scrolled_window.set_vexpand(true);
|
||||
|
||||
let features_box = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
features_box.set_halign(Align::Center);
|
||||
|
||||
features_scrolled_window.add(&features_box);
|
||||
|
||||
features_expander.add(&features_scrolled_window);
|
||||
|
||||
container.add(&grid);
|
||||
|
||||
Self {
|
||||
container,
|
||||
device_name_label,
|
||||
version_label,
|
||||
features_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_info(&self, vulkan_info: &VulkanInfo) {
|
||||
log::trace!("Setting vulkan info: {:?}", vulkan_info);
|
||||
|
||||
self.device_name_label
|
||||
.set_markup(&format!("<b>{}</b>", vulkan_info.device_name));
|
||||
self.version_label
|
||||
.set_markup(&format!("<b>{}</b>", vulkan_info.api_version));
|
||||
|
||||
for (feature, supported) in vulkan_info.features.iter() {
|
||||
let vbox = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let feature_name_label = Label::new(Some(feature));
|
||||
|
||||
vbox.pack_start(&feature_name_label, false, false, 0);
|
||||
|
||||
let feature_supported_checkbutton = CheckButton::new();
|
||||
|
||||
feature_supported_checkbutton.set_sensitive(false);
|
||||
feature_supported_checkbutton.set_active(*supported);
|
||||
|
||||
vbox.pack_start(&feature_supported_checkbutton, false, false, 0);
|
||||
|
||||
self.features_box.pack_end(&vbox, false, false, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
mod clocks_frame;
|
||||
mod power_cap_frame;
|
||||
mod power_profile_frame;
|
||||
mod stats_grid;
|
||||
mod warning_frame;
|
||||
|
||||
use clocks_frame::ClocksSettings;
|
||||
use daemon::gpu_controller::{GpuInfo, GpuStats, PowerProfile};
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
use clocks_frame::ClocksFrame;
|
||||
use power_cap_frame::PowerCapFrame;
|
||||
use power_profile_frame::PowerProfileFrame;
|
||||
use stats_grid::StatsGrid;
|
||||
use warning_frame::WarningFrame;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OcPage {
|
||||
pub container: Box,
|
||||
stats_grid: StatsGrid,
|
||||
power_profile_frame: PowerProfileFrame,
|
||||
power_cap_frame: PowerCapFrame,
|
||||
clocks_frame: ClocksFrame,
|
||||
pub warning_frame: WarningFrame,
|
||||
}
|
||||
|
||||
impl OcPage {
|
||||
pub fn new() -> Self {
|
||||
let container = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
let warning_frame = WarningFrame::new();
|
||||
|
||||
container.pack_start(&warning_frame.container, false, true, 5);
|
||||
|
||||
let stats_grid = StatsGrid::new();
|
||||
|
||||
container.pack_start(&stats_grid.container, false, true, 5);
|
||||
|
||||
let power_cap_frame = PowerCapFrame::new();
|
||||
|
||||
container.pack_start(&power_cap_frame.container, false, true, 0);
|
||||
|
||||
let power_profile_frame = PowerProfileFrame::new();
|
||||
|
||||
container.pack_start(&power_profile_frame.container, false, true, 0);
|
||||
|
||||
let clocks_frame = ClocksFrame::new();
|
||||
|
||||
container.pack_start(&clocks_frame.container, false, true, 0);
|
||||
|
||||
Self {
|
||||
container,
|
||||
stats_grid,
|
||||
power_profile_frame,
|
||||
clocks_frame,
|
||||
warning_frame,
|
||||
power_cap_frame,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &GpuStats) {
|
||||
self.stats_grid.set_stats(stats);
|
||||
}
|
||||
|
||||
pub fn connect_clocks_reset<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.clocks_frame.connect_clocks_reset(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
{
|
||||
let f = f.clone();
|
||||
self.power_profile_frame
|
||||
.connect_power_profile_changed(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
{
|
||||
let f = f.clone();
|
||||
self.clocks_frame.connect_clocks_changed(move || {
|
||||
f();
|
||||
})
|
||||
}
|
||||
{
|
||||
self.power_cap_frame.connect_cap_changed(move || {
|
||||
f();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_power_profile(&self, profile: &Option<PowerProfile>) {
|
||||
match profile {
|
||||
Some(profile) => {
|
||||
self.power_profile_frame.show();
|
||||
self.power_profile_frame.set_active_profile(profile);
|
||||
}
|
||||
None => self.power_profile_frame.hide(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_profile(&self) -> Option<PowerProfile> {
|
||||
match self.power_profile_frame.get_visibility() {
|
||||
true => Some(self.power_profile_frame.get_selected_power_profile()),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_info(&self, info: &GpuInfo) {
|
||||
match &info.clocks_table {
|
||||
Some(clocks_table) => {
|
||||
self.clocks_frame.show();
|
||||
self.clocks_frame.set_clocks(clocks_table);
|
||||
}
|
||||
None => self.clocks_frame.hide(),
|
||||
}
|
||||
|
||||
self.power_cap_frame
|
||||
.set_data(info.power_cap, info.power_cap_max);
|
||||
}
|
||||
|
||||
pub fn get_clocks(&self) -> Option<ClocksSettings> {
|
||||
match self.clocks_frame.get_visibility() {
|
||||
true => Some(self.clocks_frame.get_settings()),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_cap(&self) -> Option<i64> {
|
||||
self.power_cap_frame.get_cap()
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
use daemon::gpu_controller::ClocksTable;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
pub struct ClocksSettings {
|
||||
pub gpu_clock: i64,
|
||||
pub vram_clock: i64,
|
||||
pub gpu_voltage: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClocksFrame {
|
||||
pub container: Frame,
|
||||
gpu_clock_adjustment: Adjustment,
|
||||
gpu_voltage_adjustment: Adjustment,
|
||||
vram_clock_adjustment: Adjustment,
|
||||
apply_button: Button,
|
||||
}
|
||||
|
||||
impl ClocksFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(None);
|
||||
|
||||
container.set_margin_start(10);
|
||||
container.set_margin_end(10);
|
||||
|
||||
container.set_shadow_type(ShadowType::None);
|
||||
|
||||
container.set_label_widget(Some(&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<span font_desc='11'><b>Maximum Clocks</b></span>");
|
||||
label
|
||||
}));
|
||||
container.set_label_align(0.2, 0.0);
|
||||
|
||||
let gpu_clock_adjustment = Adjustment::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0);
|
||||
|
||||
let gpu_voltage_adjustment = Adjustment::new(1.0, 0.0, 0.0, 0.05, 0.0, 0.0);
|
||||
|
||||
let vram_clock_adjustment = Adjustment::new(0.0, 0.0, 0.0, 1.0, 0.0, 0.0);
|
||||
|
||||
let root_grid = Grid::new();
|
||||
|
||||
root_grid.set_row_spacing(5);
|
||||
root_grid.set_column_spacing(10);
|
||||
|
||||
{
|
||||
let gpu_clock_scale = Scale::new(Orientation::Horizontal, Some(&gpu_clock_adjustment));
|
||||
|
||||
gpu_clock_scale.set_hexpand(true); // Affects the grid column and all scales
|
||||
|
||||
gpu_clock_scale.set_value_pos(PositionType::Right);
|
||||
|
||||
root_grid.attach(&gpu_clock_scale, 1, 0, 1, 1);
|
||||
|
||||
root_grid.attach_next_to(
|
||||
&Label::new(Some("GPU Clock (MHz)")),
|
||||
Some(&gpu_clock_scale),
|
||||
PositionType::Left,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let gpu_voltage_scale =
|
||||
Scale::new(Orientation::Horizontal, Some(&gpu_voltage_adjustment));
|
||||
|
||||
gpu_voltage_scale.set_value_pos(PositionType::Right);
|
||||
|
||||
gpu_voltage_scale.set_digits(3);
|
||||
gpu_voltage_scale.set_round_digits(3);
|
||||
|
||||
root_grid.attach(&gpu_voltage_scale, 1, 1, 1, 1);
|
||||
|
||||
root_grid.attach_next_to(
|
||||
&Label::new(Some("GPU Voltage (V)")),
|
||||
Some(&gpu_voltage_scale),
|
||||
PositionType::Left,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let vram_clock_scale =
|
||||
Scale::new(Orientation::Horizontal, Some(&vram_clock_adjustment));
|
||||
|
||||
vram_clock_scale.set_value_pos(PositionType::Right);
|
||||
|
||||
root_grid.attach(&vram_clock_scale, 1, 2, 1, 1);
|
||||
|
||||
root_grid.attach_next_to(
|
||||
&Label::new(Some("VRAM Clock (MHz)")),
|
||||
Some(&vram_clock_scale),
|
||||
PositionType::Left,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
let apply_button = Button::new();
|
||||
|
||||
{
|
||||
apply_button.set_label("Reset");
|
||||
|
||||
root_grid.attach(&apply_button, 0, 3, 2, 1);
|
||||
|
||||
container.add(&root_grid);
|
||||
}
|
||||
|
||||
Self {
|
||||
container,
|
||||
gpu_clock_adjustment,
|
||||
gpu_voltage_adjustment,
|
||||
vram_clock_adjustment,
|
||||
apply_button,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_visibility(&self) -> bool {
|
||||
self.container.get_visible()
|
||||
}
|
||||
|
||||
pub fn set_clocks(&self, clocks_table: &ClocksTable) {
|
||||
match clocks_table {
|
||||
ClocksTable::Old(clocks_table) => {
|
||||
self.gpu_clock_adjustment
|
||||
.set_lower(clocks_table.gpu_clocks_range.0 as f64);
|
||||
self.gpu_clock_adjustment
|
||||
.set_upper(clocks_table.gpu_clocks_range.1 as f64);
|
||||
|
||||
self.gpu_voltage_adjustment
|
||||
.set_lower(clocks_table.voltage_range.0 as f64 / 1000.0);
|
||||
self.gpu_voltage_adjustment
|
||||
.set_upper(clocks_table.voltage_range.1 as f64 / 1000.0);
|
||||
|
||||
self.vram_clock_adjustment
|
||||
.set_lower(clocks_table.mem_clocks_range.0 as f64);
|
||||
self.vram_clock_adjustment
|
||||
.set_upper(clocks_table.mem_clocks_range.1 as f64);
|
||||
|
||||
let (gpu_clockspeed, gpu_voltage) =
|
||||
clocks_table.gpu_power_levels.iter().next_back().unwrap().1;
|
||||
|
||||
self.gpu_clock_adjustment.set_value(*gpu_clockspeed as f64);
|
||||
|
||||
self.gpu_voltage_adjustment
|
||||
.set_value(*gpu_voltage as f64 / 1000.0);
|
||||
|
||||
let (vram_clockspeed, _) =
|
||||
clocks_table.mem_power_levels.iter().next_back().unwrap().1;
|
||||
|
||||
self.vram_clock_adjustment
|
||||
.set_value(*vram_clockspeed as f64);
|
||||
}
|
||||
ClocksTable::New(clocks_table) => {
|
||||
self.gpu_clock_adjustment
|
||||
.set_lower(clocks_table.gpu_clocks_range.0 as f64);
|
||||
self.gpu_clock_adjustment
|
||||
.set_upper(clocks_table.gpu_clocks_range.1 as f64);
|
||||
|
||||
/* self.gpu_voltage_adjustment
|
||||
.set_lower(clocks_table.voltage_range.0 as f64 / 1000.0);
|
||||
self.gpu_voltage_adjustment
|
||||
.set_upper(clocks_table.voltage_range.1 as f64 / 1000.0);*/
|
||||
|
||||
self.vram_clock_adjustment
|
||||
.set_lower(clocks_table.mem_clocks_range.0 as f64);
|
||||
self.vram_clock_adjustment
|
||||
.set_upper(clocks_table.mem_clocks_range.1 as f64);
|
||||
|
||||
self.gpu_clock_adjustment
|
||||
.set_value(clocks_table.current_gpu_clocks.1 as f64);
|
||||
|
||||
// self.gpu_voltage_adjustment
|
||||
// .set_value(*clocks_table.gpu_voltage as f64 / 1000.0);
|
||||
|
||||
self.vram_clock_adjustment
|
||||
.set_value(clocks_table.current_max_mem_clock as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> ClocksSettings {
|
||||
let gpu_clock = self.gpu_clock_adjustment.value() as i64;
|
||||
|
||||
let vram_clock = self.vram_clock_adjustment.value() as i64;
|
||||
|
||||
let gpu_voltage = (self.gpu_voltage_adjustment.value() * 1000.0) as i64;
|
||||
|
||||
ClocksSettings {
|
||||
gpu_clock,
|
||||
vram_clock,
|
||||
gpu_voltage,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_clocks_reset<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.apply_button.connect_clicked(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connect_clocks_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
{
|
||||
let f = f.clone();
|
||||
self.gpu_clock_adjustment.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
{
|
||||
let f = f.clone();
|
||||
self.vram_clock_adjustment.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
{
|
||||
self.gpu_voltage_adjustment.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_visible(true);
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PowerCapFrame {
|
||||
pub container: Frame,
|
||||
label: Label,
|
||||
adjustment: Adjustment,
|
||||
}
|
||||
|
||||
impl PowerCapFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(None);
|
||||
|
||||
container.set_shadow_type(ShadowType::None);
|
||||
|
||||
container.set_label_widget(Some(&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<span font_desc='11'><b>Power Usage Limit</b></span>");
|
||||
label
|
||||
}));
|
||||
container.set_label_align(0.2, 0.0);
|
||||
|
||||
let root_box = Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let label = Label::new(None);
|
||||
|
||||
root_box.pack_start(&label, false, true, 5);
|
||||
|
||||
let adjustment = Adjustment::new(0.0, 0.0, 0.0, 1.0, 10.0, 0.0);
|
||||
{
|
||||
let label = label.clone();
|
||||
adjustment.connect_value_changed(move |adj| {
|
||||
label.set_markup(&format!("{}/{} W", adj.value().round(), adj.upper()));
|
||||
});
|
||||
}
|
||||
|
||||
let scale = Scale::new(Orientation::Horizontal, Some(&adjustment));
|
||||
|
||||
scale.set_draw_value(false);
|
||||
|
||||
root_box.pack_start(&scale, true, true, 5);
|
||||
|
||||
container.add(&root_box);
|
||||
|
||||
Self {
|
||||
container,
|
||||
label,
|
||||
adjustment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_data(&self, power_cap: Option<i64>, power_cap_max: Option<i64>) {
|
||||
if let Some(power_cap_max) = power_cap_max {
|
||||
self.adjustment.set_upper(power_cap_max as f64);
|
||||
} else {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
if let Some(power_cap) = power_cap {
|
||||
self.adjustment.set_value(power_cap as f64);
|
||||
} else {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cap(&self) -> Option<i64> {
|
||||
// Using match gives a warning that floats shouldn't be used in patterns
|
||||
let cap = self.adjustment.value();
|
||||
if cap == 0.0 {
|
||||
None
|
||||
} else {
|
||||
Some(cap as i64)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_cap_changed<F: Fn() + 'static>(&self, f: F) {
|
||||
self.adjustment.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
use daemon::gpu_controller::PowerProfile;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PowerProfileFrame {
|
||||
pub container: Frame,
|
||||
combo_box: ComboBoxText,
|
||||
description_label: Label,
|
||||
}
|
||||
|
||||
impl PowerProfileFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(None);
|
||||
|
||||
container.set_shadow_type(ShadowType::None);
|
||||
|
||||
container.set_label_widget(Some(&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<span font_desc='11'><b>Power Profile</b></span>");
|
||||
label
|
||||
}));
|
||||
container.set_label_align(0.2, 0.0);
|
||||
|
||||
let root_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let combo_box = ComboBoxText::new();
|
||||
|
||||
combo_box.append(Some("0"), "Automatic");
|
||||
combo_box.append(Some("1"), "Highest clocks");
|
||||
combo_box.append(Some("2"), "Lowest clocks");
|
||||
|
||||
root_box.pack_start(&combo_box, false, true, 5);
|
||||
|
||||
let description_label = Label::new(Some("A description is supposed to be here"));
|
||||
|
||||
root_box.pack_start(&description_label, false, true, 5);
|
||||
|
||||
{
|
||||
let description_label = description_label.clone();
|
||||
combo_box.connect_changed(move |combobox| match combobox.active().unwrap() {
|
||||
0 => description_label
|
||||
.set_text("Automatically adjust GPU and VRAM clocks. (Default)"),
|
||||
1 => description_label
|
||||
.set_text("Always use the highest clockspeeds for GPU and VRAM."),
|
||||
2 => description_label
|
||||
.set_text("Always use the lowest clockspeeds for GPU and VRAM."),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
|
||||
container.add(&root_box);
|
||||
Self {
|
||||
container,
|
||||
combo_box,
|
||||
description_label,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_profile(&self, profile: &PowerProfile) {
|
||||
match profile {
|
||||
PowerProfile::Auto => self.combo_box.set_active_id(Some("0")),
|
||||
PowerProfile::High => self.combo_box.set_active_id(Some("1")),
|
||||
PowerProfile::Low => self.combo_box.set_active_id(Some("2")),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn connect_power_profile_changed<F: Fn() + 'static>(&self, f: F) {
|
||||
self.combo_box.connect_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_selected_power_profile(&self) -> PowerProfile {
|
||||
match self.combo_box.active().unwrap() {
|
||||
0 => PowerProfile::Auto,
|
||||
1 => PowerProfile::High,
|
||||
2 => PowerProfile::Low,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn get_visibility(&self) -> bool {
|
||||
self.container.get_visible()
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WarningFrame {
|
||||
pub container: Frame,
|
||||
}
|
||||
|
||||
impl WarningFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(Some("Overclocking information"));
|
||||
|
||||
container.set_label_align(0.3, 0.5);
|
||||
|
||||
let warning_label = Label::new(None);
|
||||
|
||||
warning_label.set_line_wrap(true);
|
||||
warning_label.set_markup("Overclocking support is not enabled! To enable overclocking support, you need to add <b>amdgpu.ppfeaturemask=0xffffffff</b> to your kernel boot options. Look for the documentation of your distro.");
|
||||
warning_label.set_selectable(true);
|
||||
|
||||
container.add(&warning_label);
|
||||
|
||||
Self { container }
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoftwarePage {
|
||||
pub container: Grid,
|
||||
lact_version_label: Label,
|
||||
}
|
||||
|
||||
impl SoftwarePage {
|
||||
pub fn new() -> Self {
|
||||
let container = Grid::new();
|
||||
|
||||
container.set_margin_start(5);
|
||||
container.set_margin_end(5);
|
||||
container.set_margin_bottom(5);
|
||||
container.set_margin_top(5);
|
||||
|
||||
container.set_column_spacing(10);
|
||||
|
||||
container.attach(
|
||||
&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<b>LACT Version:</b>");
|
||||
label.set_halign(Align::End);
|
||||
label.set_hexpand(true);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
let lact_version_label = Label::new(None);
|
||||
|
||||
let lact_version = env!("CARGO_PKG_VERSION");
|
||||
let lact_release_type = match cfg!(debug_assertions) {
|
||||
true => "debug",
|
||||
false => "release",
|
||||
};
|
||||
|
||||
lact_version_label.set_markup(&format!("{}-{}", lact_version, lact_release_type));
|
||||
|
||||
lact_version_label.set_hexpand(true);
|
||||
lact_version_label.set_halign(Align::Start);
|
||||
|
||||
container.attach(&lact_version_label, 1, 0, 1, 1);
|
||||
|
||||
Self {
|
||||
container,
|
||||
lact_version_label,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,208 +0,0 @@
|
||||
mod fan_curve_frame;
|
||||
|
||||
use daemon::gpu_controller::{FanControlInfo, GpuStats};
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fan_curve_frame::FanCurveFrame;
|
||||
|
||||
pub struct ThermalsSettings {
|
||||
pub automatic_fan_control_enabled: bool,
|
||||
pub curve: BTreeMap<i64, f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThermalsPage {
|
||||
pub container: Box,
|
||||
temp_label: Label,
|
||||
fan_speed_label: Label,
|
||||
fan_control_enabled_switch: Switch,
|
||||
fan_curve_frame: FanCurveFrame,
|
||||
}
|
||||
|
||||
impl ThermalsPage {
|
||||
pub fn new() -> Self {
|
||||
let container = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
let grid = Grid::new();
|
||||
|
||||
grid.set_margin_start(5);
|
||||
grid.set_margin_end(5);
|
||||
grid.set_margin_bottom(5);
|
||||
grid.set_margin_top(5);
|
||||
|
||||
grid.set_column_homogeneous(true);
|
||||
|
||||
grid.set_row_spacing(7);
|
||||
grid.set_column_spacing(5);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Temperatures:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let temp_label = Label::new(None);
|
||||
temp_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&temp_label, 2, 0, 1, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Fan speed:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let fan_speed_label = Label::new(None);
|
||||
fan_speed_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&fan_speed_label, 2, 1, 1, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Automatic fan control:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let fan_control_enabled_switch = Switch::new();
|
||||
|
||||
fan_control_enabled_switch.set_active(true);
|
||||
fan_control_enabled_switch.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&fan_control_enabled_switch, 2, 2, 1, 1);
|
||||
|
||||
container.pack_start(&grid, false, false, 5);
|
||||
|
||||
let fan_curve_frame = FanCurveFrame::new();
|
||||
|
||||
container.pack_start(&fan_curve_frame.container, true, true, 5);
|
||||
|
||||
// Show/hide fan curve when the switch is toggled
|
||||
{
|
||||
let fan_curve_frame = fan_curve_frame.clone();
|
||||
fan_control_enabled_switch.connect_changed_active(move |switch| {
|
||||
log::trace!("Fan control switch toggled");
|
||||
if switch.state() {
|
||||
{
|
||||
glib::idle_add(|| {
|
||||
let diag = MessageDialog::new(None::<&Window>, DialogFlags::empty(), MessageType::Warning, ButtonsType::Ok,
|
||||
"Warning! Due to a driver bug, a reboot may be required for fan control to properly switch back to automatic.");
|
||||
diag.run();
|
||||
diag.hide();
|
||||
glib::Continue(false)
|
||||
});
|
||||
}
|
||||
|
||||
fan_curve_frame.hide();
|
||||
} else {
|
||||
fan_curve_frame.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
container,
|
||||
temp_label,
|
||||
fan_speed_label,
|
||||
fan_control_enabled_switch,
|
||||
fan_curve_frame,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_thermals_info(&self, stats: &GpuStats) {
|
||||
self.temp_label.set_markup(&format!("<b>{}</b>", {
|
||||
let mut temperatures = Vec::new();
|
||||
|
||||
for (label, temp) in stats.temperatures.iter() {
|
||||
temperatures.push(format!("{}: {}°C", label, temp.current));
|
||||
}
|
||||
|
||||
temperatures.sort();
|
||||
|
||||
if !temperatures.is_empty() {
|
||||
temperatures.join("\n")
|
||||
} else {
|
||||
String::from("No sensors found")
|
||||
}
|
||||
}));
|
||||
|
||||
match stats.fan_speed {
|
||||
Some(fan_speed) => self.fan_speed_label.set_markup(&format!(
|
||||
"<b>{} RPM ({}%)</b>",
|
||||
fan_speed,
|
||||
(fan_speed as f64 / stats.max_fan_speed.unwrap() as f64 * 100.0).round()
|
||||
)),
|
||||
None => self.fan_speed_label.set_text("No fan detected"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ventilation_info(&self, fan_control_info: FanControlInfo) {
|
||||
log::info!("Setting fan control info {:?}", fan_control_info);
|
||||
|
||||
self.fan_control_enabled_switch.set_visible(true);
|
||||
|
||||
self.fan_control_enabled_switch
|
||||
.set_active(!fan_control_info.enabled);
|
||||
|
||||
if !fan_control_info.enabled {
|
||||
self.fan_curve_frame.hide();
|
||||
} else {
|
||||
self.fan_curve_frame.show();
|
||||
}
|
||||
|
||||
self.fan_curve_frame.set_curve(&fan_control_info.curve);
|
||||
}
|
||||
|
||||
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
// Fan control switch toggled
|
||||
{
|
||||
let f = f.clone();
|
||||
self.fan_control_enabled_switch
|
||||
.connect_changed_active(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
// Fan curve adjusted
|
||||
{
|
||||
let f = f.clone();
|
||||
self.fan_curve_frame.connect_adjusted(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_thermals_settings(&self) -> ThermalsSettings {
|
||||
let automatic_fan_control_enabled = self.fan_control_enabled_switch.state();
|
||||
let curve = self.fan_curve_frame.get_curve();
|
||||
|
||||
ThermalsSettings {
|
||||
automatic_fan_control_enabled,
|
||||
curve,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_fan_controls(&self) {
|
||||
self.fan_control_enabled_switch.set_visible(false);
|
||||
self.fan_curve_frame.hide();
|
||||
}
|
||||
}
|
@ -1,217 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FanCurveFrame {
|
||||
pub container: Frame,
|
||||
adjustment_1: Adjustment,
|
||||
adjustment_2: Adjustment,
|
||||
adjustment_3: Adjustment,
|
||||
adjustment_4: Adjustment,
|
||||
adjustment_5: Adjustment,
|
||||
}
|
||||
|
||||
impl FanCurveFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(Some("Fan Curve"));
|
||||
|
||||
container.set_margin_start(10);
|
||||
container.set_margin_end(10);
|
||||
container.set_margin_bottom(10);
|
||||
container.set_margin_top(10);
|
||||
|
||||
container.set_label_align(0.35, 0.5);
|
||||
|
||||
// container.set_shadow_type(ShadowType::None);
|
||||
//
|
||||
let root_grid = Grid::new();
|
||||
|
||||
// PWM Percentage Labels
|
||||
{
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("PWM %"));
|
||||
label.set_angle(90.0);
|
||||
label.set_vexpand(true); // This expands the entire top section of the grid, including the scales
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
5,
|
||||
);
|
||||
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("0"));
|
||||
label.set_angle(90.0);
|
||||
label
|
||||
},
|
||||
1,
|
||||
4,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("25"));
|
||||
label.set_angle(90.0);
|
||||
label
|
||||
},
|
||||
1,
|
||||
3,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("50"));
|
||||
label.set_angle(90.0);
|
||||
label
|
||||
},
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("75"));
|
||||
label.set_angle(90.0);
|
||||
label
|
||||
},
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("100"));
|
||||
label.set_angle(90.0);
|
||||
label
|
||||
},
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
// Temperature threshold labels
|
||||
{
|
||||
root_grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Temperature °C"));
|
||||
label.set_hexpand(true);
|
||||
label
|
||||
},
|
||||
2,
|
||||
7,
|
||||
5,
|
||||
1,
|
||||
);
|
||||
|
||||
root_grid.attach(&Label::new(Some("20")), 2, 6, 1, 1);
|
||||
root_grid.attach(&Label::new(Some("40")), 3, 6, 1, 1);
|
||||
root_grid.attach(&Label::new(Some("60")), 4, 6, 1, 1);
|
||||
root_grid.attach(&Label::new(Some("80")), 5, 6, 1, 1);
|
||||
root_grid.attach(&Label::new(Some("100")), 6, 6, 1, 1);
|
||||
}
|
||||
|
||||
// The actual adjustments
|
||||
let adjustment_1 = Adjustment::new(0.0, 0.0, 100.0, 1.0, 0.0, 0.0); // 20 °C
|
||||
let adjustment_2 = Adjustment::new(0.0, 0.0, 100.0, 1.0, 0.0, 0.0); // 40 °C
|
||||
let adjustment_3 = Adjustment::new(0.0, 0.0, 100.0, 1.0, 0.0, 0.0); // 60 °C
|
||||
let adjustment_4 = Adjustment::new(0.0, 0.0, 100.0, 1.0, 0.0, 0.0); // 80 °C
|
||||
let adjustment_5 = Adjustment::new(0.0, 0.0, 100.0, 1.0, 0.0, 0.0); // 100 °C
|
||||
|
||||
// Scales for the adjustments
|
||||
{
|
||||
let adjustments = [
|
||||
&adjustment_1,
|
||||
&adjustment_2,
|
||||
&adjustment_3,
|
||||
&adjustment_4,
|
||||
&adjustment_5,
|
||||
];
|
||||
|
||||
for i in 0..adjustments.len() {
|
||||
let adj = adjustments[i];
|
||||
|
||||
root_grid.attach(
|
||||
&{
|
||||
let scale = Scale::new(Orientation::Vertical, Some(adj));
|
||||
scale.set_draw_value(false);
|
||||
scale.set_inverted(true);
|
||||
scale
|
||||
},
|
||||
i as i32 + 2,
|
||||
0,
|
||||
1,
|
||||
5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
container.add(&root_grid);
|
||||
|
||||
Self {
|
||||
container,
|
||||
adjustment_1,
|
||||
adjustment_2,
|
||||
adjustment_3,
|
||||
adjustment_4,
|
||||
adjustment_5,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_curve(&self, curve: &BTreeMap<i64, f64>) {
|
||||
self.adjustment_1.set_value(*curve.get(&20).unwrap());
|
||||
self.adjustment_2.set_value(*curve.get(&40).unwrap());
|
||||
self.adjustment_3.set_value(*curve.get(&60).unwrap());
|
||||
self.adjustment_4.set_value(*curve.get(&80).unwrap());
|
||||
self.adjustment_5.set_value(*curve.get(&100).unwrap());
|
||||
}
|
||||
|
||||
pub fn get_curve(&self) -> BTreeMap<i64, f64> {
|
||||
let mut curve = BTreeMap::new();
|
||||
|
||||
curve.insert(20, self.adjustment_1.value());
|
||||
curve.insert(40, self.adjustment_2.value());
|
||||
curve.insert(60, self.adjustment_3.value());
|
||||
curve.insert(80, self.adjustment_4.value());
|
||||
curve.insert(100, self.adjustment_5.value());
|
||||
|
||||
curve
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
log::info!("Manual fan control enaged, showing fan curve");
|
||||
self.container.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
log::info!("Manual fan control disenaged, hiding fan curve");
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn connect_adjusted<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
let adjustments = [
|
||||
&self.adjustment_1,
|
||||
&self.adjustment_2,
|
||||
&self.adjustment_3,
|
||||
&self.adjustment_4,
|
||||
&self.adjustment_5,
|
||||
];
|
||||
|
||||
for adj in adjustments.iter() {
|
||||
let f = f.clone();
|
||||
adj.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
use std::thread;
|
||||
|
||||
use app::App;
|
||||
use daemon::{daemon_connection::DaemonConnection, Daemon};
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
mod app;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
if gtk::init().is_err() {
|
||||
panic!("Cannot initialize GTK");
|
||||
}
|
||||
|
||||
let connection = connect_daemon();
|
||||
|
||||
ask_for_online_update(&connection);
|
||||
|
||||
let app = App::new(connection);
|
||||
|
||||
app.run().unwrap();
|
||||
}
|
||||
|
||||
fn ask_for_online_update(connection: &DaemonConnection) {
|
||||
let mut config = connection.get_config().unwrap();
|
||||
|
||||
if let None = config.allow_online_update {
|
||||
log::trace!("Online access permission not configured! Showing the dialog");
|
||||
|
||||
let diag = MessageDialog::new(
|
||||
None::<&Window>,
|
||||
DialogFlags::empty(),
|
||||
MessageType::Warning,
|
||||
ButtonsType::YesNo,
|
||||
"Do you wish to use the online database for GPU identification?",
|
||||
);
|
||||
match diag.run() {
|
||||
ResponseType::Yes => config.allow_online_update = Some(true),
|
||||
ResponseType::No => config.allow_online_update = Some(false),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
diag.hide();
|
||||
|
||||
connection.set_config(config).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn connect_daemon() -> DaemonConnection {
|
||||
match DaemonConnection::new() {
|
||||
Ok(connection) => {
|
||||
println!("Connection to daemon established");
|
||||
connection
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error {:?} connecting to daemon", e);
|
||||
println!("Starting unprivileged daemon instance");
|
||||
|
||||
thread::spawn(move || {
|
||||
let daemon = Daemon::new(true);
|
||||
daemon.listen();
|
||||
});
|
||||
|
||||
let dialog = MessageDialog::new(
|
||||
None::<>k::Window>,
|
||||
DialogFlags::empty(),
|
||||
gtk::MessageType::Warning,
|
||||
gtk::ButtonsType::Ok,
|
||||
"Unable to connect to daemon. Running in unprivileged mode.",
|
||||
);
|
||||
|
||||
dialog.run();
|
||||
dialog.close();
|
||||
|
||||
DaemonConnection::new().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
9
lact-cli/Cargo.toml
Normal file
9
lact-cli/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "lact-cli"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lact-client = { path = "../lact-client" }
|
||||
anyhow = "1.0.69"
|
||||
clap = { version = "4.1.6", features = ["derive"] }
|
35
lact-cli/src/args.rs
Normal file
35
lact-cli/src/args.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use lact_client::DaemonClient;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about)]
|
||||
pub struct CliArgs {
|
||||
pub gpu_id: Option<String>,
|
||||
#[command(subcommand)]
|
||||
pub subcommand: CliCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum CliCommand {
|
||||
/// List GPUs
|
||||
ListGpus,
|
||||
/// Show GPU info
|
||||
Info,
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
pub fn gpu_ids(&self, client: &DaemonClient) -> Vec<String> {
|
||||
match self.gpu_id {
|
||||
Some(ref id) => vec![id.clone()],
|
||||
None => {
|
||||
let buffer = client.list_devices().expect("Could not list GPUs");
|
||||
buffer
|
||||
.inner()
|
||||
.expect("Could not deserialize GPUs response")
|
||||
.into_iter()
|
||||
.map(|entry| entry.id.to_owned())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
lact-cli/src/lib.rs
Normal file
49
lact-cli/src/lib.rs
Normal file
@ -0,0 +1,49 @@
|
||||
pub mod args;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use args::{CliArgs, CliCommand};
|
||||
use lact_client::DaemonClient;
|
||||
|
||||
pub fn run(args: CliArgs) -> Result<()> {
|
||||
let client = DaemonClient::connect()?;
|
||||
|
||||
let f = match args.subcommand {
|
||||
CliCommand::ListGpus => list_gpus,
|
||||
CliCommand::Info => info,
|
||||
};
|
||||
f(&args, &client)
|
||||
}
|
||||
|
||||
fn list_gpus(_: &CliArgs, client: &DaemonClient) -> Result<()> {
|
||||
let buffer = client.list_devices()?;
|
||||
for entry in buffer.inner()? {
|
||||
let id = entry.id;
|
||||
if let Some(name) = entry.name {
|
||||
println!("{id} ({name})");
|
||||
} else {
|
||||
println!("{id}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info(args: &CliArgs, client: &DaemonClient) -> Result<()> {
|
||||
for id in args.gpu_ids(client) {
|
||||
let info_buffer = client.get_device_info(&id)?;
|
||||
let info = info_buffer.inner()?;
|
||||
let pci_info = info.pci_info.context("GPU reports no pci info")?;
|
||||
|
||||
if let Some(ref vendor) = pci_info.device_pci_info.vendor {
|
||||
println!("GPU Vendor: {vendor}");
|
||||
}
|
||||
if let Some(ref model) = pci_info.device_pci_info.model {
|
||||
println!("GPU Model: {model}");
|
||||
}
|
||||
println!("Driver in use: {}", info.driver);
|
||||
if let Some(ref vbios_version) = info.vbios_version {
|
||||
println!("VBIOS version: {vbios_version}");
|
||||
}
|
||||
println!("Link: {:?}", info.link_info);
|
||||
}
|
||||
Ok(())
|
||||
}
|
12
lact-client/Cargo.toml
Normal file
12
lact-client/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "lact-client"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
lact-schema = { path = "../lact-schema" }
|
||||
anyhow = "1.0.69"
|
||||
nix = { version = "0.26.2", default-features = false }
|
||||
serde = "1.0.152"
|
||||
tracing = "0.1.37"
|
||||
serde_json = "1.0.93"
|
143
lact-client/src/lib.rs
Normal file
143
lact-client/src/lib.rs
Normal file
@ -0,0 +1,143 @@
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub use lact_schema as schema;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use nix::unistd::getuid;
|
||||
use schema::{
|
||||
request::SetClocksCommand, ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanCurveMap,
|
||||
PerformanceLevel, Request, Response, SystemInfo,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
marker::PhantomData,
|
||||
ops::DerefMut,
|
||||
os::unix::net::UnixStream,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonClient {
|
||||
stream: Arc<Mutex<(BufReader<UnixStream>, UnixStream)>>,
|
||||
pub embedded: bool,
|
||||
}
|
||||
|
||||
impl DaemonClient {
|
||||
pub fn connect() -> anyhow::Result<Self> {
|
||||
let path =
|
||||
get_socket_path().context("Could not connect to daemon: socket file not found")?;
|
||||
info!("connecting to service at {path:?}");
|
||||
let stream = UnixStream::connect(path).context("Could not connect to daemon")?;
|
||||
Self::from_stream(stream, false)
|
||||
}
|
||||
|
||||
pub fn from_stream(stream: UnixStream, embedded: bool) -> anyhow::Result<Self> {
|
||||
let reader = BufReader::new(stream.try_clone()?);
|
||||
Ok(Self {
|
||||
stream: Arc::new(Mutex::new((reader, stream))),
|
||||
embedded,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_request<'a, T: Deserialize<'a>>(
|
||||
&self,
|
||||
request: Request,
|
||||
) -> anyhow::Result<ResponseBuffer<T>> {
|
||||
let mut stream_guard = self.stream.lock().map_err(|err| anyhow!("{err}"))?;
|
||||
let (reader, writer) = stream_guard.deref_mut();
|
||||
|
||||
if !reader.buffer().is_empty() {
|
||||
return Err(anyhow!("Another request was not processed properly"));
|
||||
}
|
||||
|
||||
let request_payload = serde_json::to_string(&request)?;
|
||||
writer.write_all(request_payload.as_bytes())?;
|
||||
writer.write_all(b"\n")?;
|
||||
|
||||
let mut response_payload = String::new();
|
||||
reader.read_line(&mut response_payload)?;
|
||||
|
||||
Ok(ResponseBuffer {
|
||||
buf: response_payload,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_devices<'a>(&self) -> anyhow::Result<ResponseBuffer<Vec<DeviceListEntry<'a>>>> {
|
||||
self.make_request(Request::ListDevices)
|
||||
}
|
||||
|
||||
pub fn set_fan_control(
|
||||
&self,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
curve: Option<FanCurveMap>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.make_request::<()>(Request::SetFanControl { id, enabled, curve })?
|
||||
.inner()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_power_cap(&self, id: &str, cap: Option<f64>) -> anyhow::Result<()> {
|
||||
self.make_request(Request::SetPowerCap { id, cap })?.inner()
|
||||
}
|
||||
|
||||
request_plain!(get_system_info, SystemInfo, SystemInfo);
|
||||
request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
|
||||
request_with_id!(get_device_stats, DeviceStats, DeviceStats);
|
||||
request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo);
|
||||
|
||||
pub fn set_performance_level(
|
||||
&self,
|
||||
id: &str,
|
||||
performance_level: PerformanceLevel,
|
||||
) -> anyhow::Result<()> {
|
||||
self.make_request(Request::SetPerformanceLevel {
|
||||
id,
|
||||
performance_level,
|
||||
})?
|
||||
.inner()
|
||||
}
|
||||
|
||||
pub fn set_clocks_value(&self, id: &str, command: SetClocksCommand) -> anyhow::Result<()> {
|
||||
self.make_request(Request::SetClocksValue { id, command })?
|
||||
.inner()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_socket_path() -> Option<PathBuf> {
|
||||
let root_path = PathBuf::from("/var/run/lactd.sock");
|
||||
|
||||
if root_path.exists() {
|
||||
return Some(root_path);
|
||||
}
|
||||
|
||||
let uid = getuid();
|
||||
let user_path = PathBuf::from(format!("/var/run/user/{}/lactd.sock", uid));
|
||||
|
||||
if user_path.exists() {
|
||||
Some(user_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResponseBuffer<T> {
|
||||
buf: String,
|
||||
_phantom: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: Deserialize<'a>> ResponseBuffer<T> {
|
||||
pub fn inner(&'a self) -> anyhow::Result<T> {
|
||||
let response: Response<T> = serde_json::from_str(&self.buf)
|
||||
.context("Could not deserialize response from daemon")?;
|
||||
match response {
|
||||
Response::Ok(data) => Ok(data),
|
||||
Response::Error(err) => Err(anyhow!("Got error from daemon: {err}")),
|
||||
}
|
||||
}
|
||||
}
|
15
lact-client/src/macros.rs
Normal file
15
lact-client/src/macros.rs
Normal file
@ -0,0 +1,15 @@
|
||||
macro_rules! request_with_id {
|
||||
($name:ident, $variant:ident, $response:ty) => {
|
||||
pub fn $name(&self, id: &str) -> anyhow::Result<ResponseBuffer<$response>> {
|
||||
self.make_request(Request::$variant { id })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! request_plain {
|
||||
($name:ident, $variant:ident, $response:ty) => {
|
||||
pub fn $name(&self) -> anyhow::Result<ResponseBuffer<$response>> {
|
||||
self.make_request(Request::$variant)
|
||||
}
|
||||
};
|
||||
}
|
36
lact-daemon/Cargo.toml
Normal file
36
lact-daemon/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "lact-daemon"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
amdgpu-sysfs = { version = "0.9.3", features = ["serde"] }
|
||||
anyhow = "1.0"
|
||||
bincode = "1.3"
|
||||
nix = "0.26"
|
||||
pciid-parser = { version = "0.6", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
tokio = { version = "1.25.0", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"net",
|
||||
"io-util",
|
||||
"time",
|
||||
"signal",
|
||||
"sync",
|
||||
] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
vulkano = { git = "https://github.com/vulkano-rs/vulkano" }
|
||||
lact-schema = { path = "../lact-schema" }
|
||||
futures = { version = "0.3.26", default-features = false, features = [
|
||||
"std",
|
||||
"alloc",
|
||||
] }
|
||||
serde_with = { version = "2.2.0", default-features = false, features = [
|
||||
"macros",
|
||||
] }
|
126
lact-daemon/src/config.rs
Normal file
126
lact-daemon/src/config.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use crate::server::gpu_controller::fan_control::FanCurve;
|
||||
use anyhow::Context;
|
||||
use lact_schema::PerformanceLevel;
|
||||
use nix::unistd::getuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use tracing::debug;
|
||||
|
||||
const FILE_NAME: &str = "config.yaml";
|
||||
const DEFAULT_ADMIN_GROUPS: [&str; 2] = ["wheel", "sudo"];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Config {
|
||||
pub daemon: Daemon,
|
||||
pub gpus: HashMap<String, Gpu>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Daemon {
|
||||
pub log_level: String,
|
||||
pub admin_groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Daemon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: "info".to_owned(),
|
||||
admin_groups: DEFAULT_ADMIN_GROUPS.map(str::to_owned).to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Gpu {
|
||||
pub fan_control_enabled: bool,
|
||||
pub fan_control_settings: Option<FanControlSettings>,
|
||||
pub power_cap: Option<f64>,
|
||||
pub performance_level: Option<PerformanceLevel>,
|
||||
pub max_core_clock: Option<u32>,
|
||||
pub max_memory_clock: Option<u32>,
|
||||
pub max_voltage: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FanControlSettings {
|
||||
pub temperature_key: String,
|
||||
pub interval_ms: u64,
|
||||
pub curve: FanCurve,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> anyhow::Result<Option<Self>> {
|
||||
let path = get_path();
|
||||
if path.exists() {
|
||||
let raw_config = fs::read_to_string(path).context("Could not open config file")?;
|
||||
let config =
|
||||
serde_yaml::from_str(&raw_config).context("Could not deserialize config")?;
|
||||
Ok(Some(config))
|
||||
} else {
|
||||
let parent = path.parent().unwrap();
|
||||
fs::create_dir_all(parent)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let path = get_path();
|
||||
debug!("saving config to {path:?}");
|
||||
let raw_config = serde_yaml::to_string(self)?;
|
||||
fs::write(path, raw_config).context("Could not write config")
|
||||
}
|
||||
|
||||
pub fn load_or_create() -> anyhow::Result<Self> {
|
||||
if let Some(config) = Config::load()? {
|
||||
Ok(config)
|
||||
} else {
|
||||
let config = Config::default();
|
||||
config.save()?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_path() -> PathBuf {
|
||||
let uid = getuid();
|
||||
if uid.is_root() {
|
||||
PathBuf::from("/etc/lact").join(FILE_NAME)
|
||||
} else {
|
||||
let config_dir = PathBuf::from(env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| {
|
||||
let home = env::var("HOME").expect("$HOME variable is not set");
|
||||
format!("{home}/.config")
|
||||
}));
|
||||
config_dir.join("lact").join(FILE_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Config, Daemon, FanControlSettings, Gpu};
|
||||
use crate::server::gpu_controller::fan_control::FanCurve;
|
||||
|
||||
#[test]
|
||||
fn serde_de_full() {
|
||||
let config = Config {
|
||||
daemon: Daemon::default(),
|
||||
gpus: [(
|
||||
"my-gpu-id".to_owned(),
|
||||
Gpu {
|
||||
fan_control_enabled: true,
|
||||
fan_control_settings: Some(FanControlSettings {
|
||||
curve: FanCurve::default(),
|
||||
temperature_key: "edge".to_owned(),
|
||||
interval_ms: 500,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)]
|
||||
.into(),
|
||||
};
|
||||
let data = serde_yaml::to_string(&config).unwrap();
|
||||
let deserialized_config: Config = serde_yaml::from_str(&data).unwrap();
|
||||
assert_eq!(config, deserialized_config);
|
||||
}
|
||||
}
|
100
lact-daemon/src/fork.rs
Normal file
100
lact-daemon/src/fork.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use nix::{
|
||||
sys::wait::waitpid,
|
||||
unistd::{fork, ForkResult},
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{BufReader, Read, Write},
|
||||
mem::size_of,
|
||||
os::unix::net::UnixStream,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
pub unsafe fn run_forked<T, F>(f: F) -> anyhow::Result<T>
|
||||
where
|
||||
T: Serialize + DeserializeOwned + Debug,
|
||||
F: FnOnce() -> Result<T, String>,
|
||||
{
|
||||
let (rx, mut tx) = UnixStream::pair()?;
|
||||
let mut rx = BufReader::new(rx);
|
||||
|
||||
match fork()? {
|
||||
ForkResult::Parent { child } => {
|
||||
trace!("waiting for message from child");
|
||||
|
||||
let mut size_buf = [0u8; size_of::<usize>()];
|
||||
rx.read_exact(&mut size_buf)?;
|
||||
let size = usize::from_ne_bytes(size_buf);
|
||||
|
||||
let mut data_buf = vec![0u8; size];
|
||||
rx.read_exact(&mut data_buf)?;
|
||||
|
||||
trace!("received {} data bytes from child", data_buf.len());
|
||||
|
||||
waitpid(child, None)?;
|
||||
|
||||
let data: Result<T, String> = bincode::deserialize(&data_buf)
|
||||
.context("Could not deserialize response from child")?;
|
||||
|
||||
data.map_err(|err| anyhow!("{err}"))
|
||||
}
|
||||
ForkResult::Child => {
|
||||
let response = f();
|
||||
trace!("sending response to parent: {response:?}");
|
||||
|
||||
let send_result = (|| {
|
||||
let data = bincode::serialize(&response)?;
|
||||
tx.write_all(&data.len().to_ne_bytes())?;
|
||||
tx.write_all(&data)?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})();
|
||||
|
||||
let exit_code = match send_result {
|
||||
Ok(()) => 0,
|
||||
Err(_) => 1,
|
||||
};
|
||||
trace!("exiting child with code {exit_code}");
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::run_forked;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let response = unsafe { run_forked(|| Ok(String::from("hello"))).unwrap() };
|
||||
assert_eq!(response, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error() {
|
||||
let response =
|
||||
unsafe { run_forked::<(), _>(|| Err("something went wrong".to_owned())) }.unwrap_err();
|
||||
assert_eq!(response.to_string(), "something went wrong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vec() {
|
||||
let response = unsafe {
|
||||
run_forked(|| {
|
||||
let data = ["hello", "world", "123"].map(str::to_owned);
|
||||
Ok(data)
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(response, ["hello", "world", "123"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pci_db() {
|
||||
let db = unsafe {
|
||||
run_forked(|| pciid_parser::Database::read().map_err(|err| err.to_string())).unwrap()
|
||||
};
|
||||
assert_ne!(db.classes.len(), 0);
|
||||
}
|
||||
}
|
84
lact-daemon/src/lib.rs
Normal file
84
lact-daemon/src/lib.rs
Normal file
@ -0,0 +1,84 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
|
||||
mod config;
|
||||
mod fork;
|
||||
mod server;
|
||||
mod socket;
|
||||
|
||||
use anyhow::Context;
|
||||
use config::Config;
|
||||
use futures::future::select_all;
|
||||
use server::{handle_stream, handler::Handler, Server};
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::str::FromStr;
|
||||
use tokio::{
|
||||
runtime,
|
||||
signal::unix::{signal, SignalKind},
|
||||
};
|
||||
use tracing::{debug_span, info, Instrument, Level};
|
||||
|
||||
const SHUTDOWN_SIGNALS: [SignalKind; 4] = [
|
||||
SignalKind::terminate(),
|
||||
SignalKind::interrupt(),
|
||||
SignalKind::quit(),
|
||||
SignalKind::hangup(),
|
||||
];
|
||||
|
||||
/// Run the daemon, binding to the default socket.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error when the daemon cannot initialize.
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
let rt = runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Could not initialize tokio runtime");
|
||||
rt.block_on(async {
|
||||
let config = Config::load_or_create()?;
|
||||
|
||||
let max_level = Level::from_str(&config.daemon.log_level).context("Invalid log level")?;
|
||||
tracing_subscriber::fmt().with_max_level(max_level).init();
|
||||
|
||||
let server = Server::new(config).await?;
|
||||
let handler = server.handler.clone();
|
||||
|
||||
tokio::spawn(listen_shutdown(handler));
|
||||
server.run().await;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the daemon with a given `UnixStream`.
|
||||
/// This will NOT bind to a socket by itself, and the daemon will only be accessible via the given stream.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error when the daemon cannot initialize.
|
||||
pub fn run_embedded(stream: StdUnixStream) -> anyhow::Result<()> {
|
||||
let rt = runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Could not initialize tokio runtime");
|
||||
rt.block_on(async {
|
||||
let config = Config::default();
|
||||
let handler = Handler::new(config).await?;
|
||||
let stream = stream.try_into()?;
|
||||
|
||||
handle_stream(stream, handler).await
|
||||
})
|
||||
}
|
||||
|
||||
async fn listen_shutdown(handler: Handler) {
|
||||
let mut signals = SHUTDOWN_SIGNALS
|
||||
.map(|signal_kind| signal(signal_kind).expect("Could not listen to shutdown signal"));
|
||||
let signal_futures = signals.iter_mut().map(|signal| Box::pin(signal.recv()));
|
||||
select_all(signal_futures).await;
|
||||
|
||||
info!("cleaning up and shutting down...");
|
||||
async {
|
||||
handler.cleanup().await;
|
||||
socket::cleanup();
|
||||
}
|
||||
.instrument(debug_span!("shutdown_cleanup"))
|
||||
.await;
|
||||
std::process::exit(0);
|
||||
}
|
162
lact-daemon/src/server/gpu_controller/fan_control.rs
Normal file
162
lact-daemon/src/server/gpu_controller/fan_control.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use amdgpu_sysfs::hw_mon::Temperature;
|
||||
use anyhow::anyhow;
|
||||
use lact_schema::{default_fan_curve, FanCurveMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FanCurve(pub FanCurveMap);
|
||||
|
||||
#[allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
impl FanCurve {
|
||||
pub fn pwm_at_temp(&self, temp: Temperature) -> u8 {
|
||||
let current = temp.current.expect("No current temp");
|
||||
|
||||
// This scenario is most likely unreachable as the kernel shuts down the GPU when it reaches critical temperature
|
||||
if temp.crit.filter(|crit| current > *crit).is_some()
|
||||
|| temp.crit_hyst.filter(|hyst| current < *hyst).is_some()
|
||||
{
|
||||
warn!("GPU temperature is beyond critical values! {current}°C");
|
||||
return u8::MAX;
|
||||
}
|
||||
|
||||
let current = current as i32;
|
||||
let maybe_lower = self.0.range(..current).next_back();
|
||||
let maybe_higher = self.0.range(current..).next();
|
||||
|
||||
let percentage = match (maybe_lower, maybe_higher) {
|
||||
(Some((lower_temp, lower_speed)), Some((higher_temp, higher_speed))) => {
|
||||
let speed_ratio = (current - lower_temp) as f32 / (higher_temp - lower_temp) as f32;
|
||||
lower_speed + (higher_speed - lower_speed) * speed_ratio
|
||||
}
|
||||
(Some((_, lower_speed)), None) => *lower_speed,
|
||||
(None, Some((_, higher_speed))) => *higher_speed,
|
||||
(None, None) => panic!("Could not find fan speed on the curve! This is a bug."),
|
||||
};
|
||||
|
||||
(f32::from(u8::MAX) * percentage) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl FanCurve {
|
||||
pub fn validate(&self) -> anyhow::Result<()> {
|
||||
for percentage in self.0.values() {
|
||||
if !(0.0..=1.0).contains(percentage) {
|
||||
return Err(anyhow!("Fan speed percentage must be between 0 and 1"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FanCurve {
|
||||
fn default() -> Self {
|
||||
Self(default_fan_curve())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FanCurve;
|
||||
use amdgpu_sysfs::hw_mon::Temperature;
|
||||
|
||||
fn simple_pwm(temp: f32) -> u8 {
|
||||
let curve = FanCurve([(0, 0.0), (100, 1.0)].into());
|
||||
let temp = Temperature {
|
||||
current: Some(temp),
|
||||
crit: Some(150.0),
|
||||
crit_hyst: Some(-100.0),
|
||||
};
|
||||
curve.pwm_at_temp(temp)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_curve_middle() {
|
||||
let pwm = simple_pwm(45.0);
|
||||
assert_eq!(pwm, 114);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_curve_start() {
|
||||
let pwm = simple_pwm(0.0);
|
||||
assert_eq!(pwm, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_curve_end() {
|
||||
let pwm = simple_pwm(100.0);
|
||||
assert_eq!(pwm, 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_curve_before() {
|
||||
let pwm = simple_pwm(-5.0);
|
||||
assert_eq!(pwm, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_curve_after() {
|
||||
let pwm = simple_pwm(105.0);
|
||||
assert_eq!(pwm, 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curve_crit() {
|
||||
let curve = FanCurve([(20, 0.0), (80, 100.0)].into());
|
||||
let temp = Temperature {
|
||||
current: Some(100.0),
|
||||
crit: Some(90.0),
|
||||
crit_hyst: Some(0.0),
|
||||
};
|
||||
let pwm = curve.pwm_at_temp(temp);
|
||||
assert_eq!(pwm, 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uneven_curve() {
|
||||
let curve = FanCurve([(30, 0.0), (40, 0.1), (55, 0.9), (61, 1.0)].into());
|
||||
let pwm_at_temp = |current: f32| {
|
||||
let temp = Temperature {
|
||||
current: Some(current),
|
||||
crit: Some(90.0),
|
||||
crit_hyst: Some(0.0),
|
||||
};
|
||||
curve.pwm_at_temp(temp)
|
||||
};
|
||||
|
||||
assert_eq!(pwm_at_temp(30.0), 0);
|
||||
assert_eq!(pwm_at_temp(35.0), 12);
|
||||
assert_eq!(pwm_at_temp(40.0), 25);
|
||||
assert_eq!(pwm_at_temp(47.0), 120);
|
||||
assert_eq!(pwm_at_temp(52.0), 188);
|
||||
assert_eq!(pwm_at_temp(53.0), 202);
|
||||
assert_eq!(pwm_at_temp(54.0), 215);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_curve() {
|
||||
let curve = FanCurve::default();
|
||||
let pwm_at_temp = |current: f32| {
|
||||
let temp = Temperature {
|
||||
current: Some(current),
|
||||
crit: Some(90.0),
|
||||
crit_hyst: Some(0.0),
|
||||
};
|
||||
curve.pwm_at_temp(temp)
|
||||
};
|
||||
assert_eq!(pwm_at_temp(20.0), 0);
|
||||
assert_eq!(pwm_at_temp(30.0), 0);
|
||||
assert_eq!(pwm_at_temp(33.0), 15);
|
||||
assert_eq!(pwm_at_temp(60.0), 127);
|
||||
assert_eq!(pwm_at_temp(65.0), 159);
|
||||
assert_eq!(pwm_at_temp(70.0), 191);
|
||||
assert_eq!(pwm_at_temp(79.0), 248);
|
||||
assert_eq!(pwm_at_temp(85.0), 255);
|
||||
assert_eq!(pwm_at_temp(100.0), 255);
|
||||
assert_eq!(pwm_at_temp(-5.0), 255);
|
||||
}
|
||||
}
|
367
lact-daemon/src/server/gpu_controller/mod.rs
Normal file
367
lact-daemon/src/server/gpu_controller/mod.rs
Normal file
@ -0,0 +1,367 @@
|
||||
pub mod fan_control;
|
||||
|
||||
use self::fan_control::FanCurve;
|
||||
use super::vulkan::get_vulkan_info;
|
||||
use crate::{config, fork::run_forked};
|
||||
use amdgpu_sysfs::{
|
||||
error::Error,
|
||||
gpu_handle::GpuHandle,
|
||||
hw_mon::{FanControlMethod, HwMon},
|
||||
sysfs::SysFS,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use lact_schema::{
|
||||
ClocksInfo, ClocksTable, ClockspeedStats, DeviceInfo, DeviceStats, FanStats, GpuPciInfo,
|
||||
LinkInfo, PciInfo, PerformanceLevel, PowerStats, VoltageStats, VramStats,
|
||||
};
|
||||
use pciid_parser::Database;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{select, sync::Notify, task::JoinHandle, time::sleep};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
type FanControlHandle = (Arc<Notify>, JoinHandle<()>);
|
||||
|
||||
pub struct GpuController {
|
||||
pub handle: GpuHandle,
|
||||
pub pci_info: Option<GpuPciInfo>,
|
||||
pub fan_control_handle: Mutex<Option<FanControlHandle>>,
|
||||
}
|
||||
|
||||
impl GpuController {
|
||||
pub fn new_from_path(sysfs_path: PathBuf) -> anyhow::Result<Self> {
|
||||
let handle = GpuHandle::new_from_path(sysfs_path)
|
||||
.map_err(|error| anyhow!("failed to initialize gpu handle: {error}"))?;
|
||||
|
||||
let mut device_pci_info = None;
|
||||
let mut subsystem_pci_info = None;
|
||||
|
||||
if let Some((vendor_id, model_id)) = handle.get_pci_id() {
|
||||
device_pci_info = Some(PciInfo {
|
||||
vendor_id: vendor_id.to_owned(),
|
||||
vendor: None,
|
||||
model_id: model_id.to_owned(),
|
||||
model: None,
|
||||
});
|
||||
|
||||
if let Some((subsys_vendor_id, subsys_model_id)) = handle.get_pci_subsys_id() {
|
||||
let (new_device_info, new_subsystem_info) = unsafe {
|
||||
run_forked(|| {
|
||||
let pci_db = Database::read().map_err(|err| err.to_string())?;
|
||||
let pci_device_info = pci_db.get_device_info(
|
||||
vendor_id,
|
||||
model_id,
|
||||
subsys_vendor_id,
|
||||
subsys_model_id,
|
||||
);
|
||||
|
||||
let device_pci_info = PciInfo {
|
||||
vendor_id: vendor_id.to_owned(),
|
||||
vendor: pci_device_info.vendor_name.map(str::to_owned),
|
||||
model_id: model_id.to_owned(),
|
||||
model: pci_device_info.device_name.map(str::to_owned),
|
||||
};
|
||||
let subsystem_pci_info = PciInfo {
|
||||
vendor_id: subsys_vendor_id.to_owned(),
|
||||
vendor: pci_device_info.subvendor_name.map(str::to_owned),
|
||||
model_id: subsys_model_id.to_owned(),
|
||||
model: pci_device_info.subdevice_name.map(str::to_owned),
|
||||
};
|
||||
Ok((device_pci_info, subsystem_pci_info))
|
||||
})?
|
||||
};
|
||||
device_pci_info = Some(new_device_info);
|
||||
subsystem_pci_info = Some(new_subsystem_info);
|
||||
}
|
||||
}
|
||||
|
||||
let pci_info = device_pci_info.and_then(|device_pci_info| {
|
||||
Some(GpuPciInfo {
|
||||
device_pci_info,
|
||||
subsystem_pci_info: subsystem_pci_info?,
|
||||
})
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
handle,
|
||||
pci_info,
|
||||
fan_control_handle: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_id(&self) -> anyhow::Result<String> {
|
||||
let handle = &self.handle;
|
||||
let pci_id = handle.get_pci_id().context("Device has no vendor id")?;
|
||||
let pci_subsys_id = handle
|
||||
.get_pci_subsys_id()
|
||||
.context("Device has no subsys id")?;
|
||||
let pci_slot_name = handle
|
||||
.get_pci_slot_name()
|
||||
.context("Device has no pci slot")?;
|
||||
|
||||
Ok(format!(
|
||||
"{}:{}-{}:{}-{}",
|
||||
pci_id.0, pci_id.1, pci_subsys_id.0, pci_subsys_id.1, pci_slot_name
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_path(&self) -> &Path {
|
||||
self.handle.get_path()
|
||||
}
|
||||
|
||||
fn first_hw_mon(&self) -> anyhow::Result<&HwMon> {
|
||||
self.handle
|
||||
.hw_monitors
|
||||
.first()
|
||||
.context("GPU has no hardware monitor")
|
||||
}
|
||||
|
||||
pub fn get_info(&self) -> DeviceInfo {
|
||||
let vulkan_info = self.pci_info.as_ref().and_then(|pci_info| {
|
||||
match get_vulkan_info(
|
||||
&pci_info.device_pci_info.vendor_id,
|
||||
&pci_info.device_pci_info.model_id,
|
||||
) {
|
||||
Ok(info) => Some(info),
|
||||
Err(err) => {
|
||||
warn!("could not load vulkan info: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
let pci_info = self.pci_info.as_ref().map(Cow::Borrowed);
|
||||
let driver = self.handle.get_driver();
|
||||
let vbios_version = self.handle.get_vbios_version().ok();
|
||||
let link_info = self.get_link_info();
|
||||
|
||||
DeviceInfo {
|
||||
pci_info,
|
||||
vulkan_info,
|
||||
driver,
|
||||
vbios_version,
|
||||
link_info,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_link_info(&self) -> LinkInfo {
|
||||
LinkInfo {
|
||||
current_width: self.handle.get_current_link_width().ok(),
|
||||
current_speed: self.handle.get_current_link_speed().ok(),
|
||||
max_width: self.handle.get_max_link_width().ok(),
|
||||
max_speed: self.handle.get_max_link_speed().ok(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> anyhow::Result<DeviceStats> {
|
||||
let fan_control_enabled = self
|
||||
.fan_control_handle
|
||||
.lock()
|
||||
.map_err(|err| anyhow!("Could not lock fan control mutex: {err}"))?
|
||||
.is_some();
|
||||
|
||||
Ok(DeviceStats {
|
||||
fan: FanStats {
|
||||
control_enabled: fan_control_enabled,
|
||||
curve: gpu_config
|
||||
.and_then(|config| config.fan_control_settings.as_ref())
|
||||
.map(|settings| settings.curve.0.clone()),
|
||||
speed_current: self.hw_mon_and_then(HwMon::get_fan_current),
|
||||
speed_max: self.hw_mon_and_then(HwMon::get_fan_max),
|
||||
speed_min: self.hw_mon_and_then(HwMon::get_fan_min),
|
||||
},
|
||||
clockspeed: ClockspeedStats {
|
||||
gpu_clockspeed: self.hw_mon_and_then(HwMon::get_gpu_clockspeed),
|
||||
vram_clockspeed: self.hw_mon_and_then(HwMon::get_vram_clockspeed),
|
||||
},
|
||||
voltage: VoltageStats {
|
||||
gpu: self.hw_mon_and_then(HwMon::get_gpu_voltage),
|
||||
northbridge: self.hw_mon_and_then(HwMon::get_northbridge_voltage),
|
||||
},
|
||||
vram: VramStats {
|
||||
total: self.handle.get_total_vram().ok(),
|
||||
used: self.handle.get_used_vram().ok(),
|
||||
},
|
||||
power: PowerStats {
|
||||
average: self.hw_mon_and_then(HwMon::get_power_average),
|
||||
cap_current: self.hw_mon_and_then(HwMon::get_power_cap),
|
||||
cap_max: self.hw_mon_and_then(HwMon::get_power_cap_max),
|
||||
cap_min: self.hw_mon_and_then(HwMon::get_power_cap_min),
|
||||
cap_default: self.hw_mon_and_then(HwMon::get_power_cap_default),
|
||||
},
|
||||
temps: self.hw_mon_map(HwMon::get_temps).unwrap_or_default(),
|
||||
busy_percent: self.handle.get_busy_percent().ok(),
|
||||
performance_level: self.handle.get_power_force_performance_level().ok(),
|
||||
core_clock_levels: self.handle.get_core_clock_levels().ok(),
|
||||
memory_clock_levels: self.handle.get_memory_clock_levels().ok(),
|
||||
pcie_clock_levels: self.handle.get_pcie_clock_levels().ok(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_clocks_info(&self) -> anyhow::Result<ClocksInfo> {
|
||||
let clocks_table = self
|
||||
.handle
|
||||
.get_clocks_table()
|
||||
.context("Clocks table not available")?;
|
||||
Ok(clocks_table.into())
|
||||
}
|
||||
|
||||
fn hw_mon_and_then<U>(&self, f: fn(&HwMon) -> Result<U, Error>) -> Option<U> {
|
||||
self.handle.hw_monitors.first().and_then(|mon| f(mon).ok())
|
||||
}
|
||||
|
||||
fn hw_mon_map<U>(&self, f: fn(&HwMon) -> U) -> Option<U> {
|
||||
self.handle.hw_monitors.first().map(f)
|
||||
}
|
||||
|
||||
async fn start_fan_control(
|
||||
&self,
|
||||
curve: FanCurve,
|
||||
temp_key: String,
|
||||
interval: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
// Stop existing task to re-apply new curve
|
||||
self.stop_fan_control(false).await?;
|
||||
|
||||
let hw_mon = self
|
||||
.handle
|
||||
.hw_monitors
|
||||
.first()
|
||||
.cloned()
|
||||
.context("This GPU has no monitor")?;
|
||||
hw_mon
|
||||
.set_fan_control_method(FanControlMethod::Manual)
|
||||
.context("Could not set fan control method")?;
|
||||
|
||||
let mut notify_guard = self
|
||||
.fan_control_handle
|
||||
.lock()
|
||||
.map_err(|err| anyhow!("Lock error: {err}"))?;
|
||||
|
||||
let notify = Arc::new(Notify::new());
|
||||
let task_notify = notify.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
select! {
|
||||
_ = sleep(interval) => (),
|
||||
_ = task_notify.notified() => break,
|
||||
}
|
||||
|
||||
let mut temps = hw_mon.get_temps();
|
||||
let temp = temps
|
||||
.remove(&temp_key)
|
||||
.expect("Could not get temperature by given key");
|
||||
let target_pwm = curve.pwm_at_temp(temp);
|
||||
trace!("fan control tick: setting pwm to {target_pwm}");
|
||||
|
||||
if let Err(err) = hw_mon.set_fan_pwm(target_pwm) {
|
||||
error!("could not set fan speed: {err}, disabling fan control");
|
||||
break;
|
||||
}
|
||||
}
|
||||
debug!("exited fan control task");
|
||||
});
|
||||
|
||||
*notify_guard = Some((notify, handle));
|
||||
|
||||
debug!(
|
||||
"started fan control with interval {}ms",
|
||||
interval.as_millis()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_fan_control(&self, reset_mode: bool) -> anyhow::Result<()> {
|
||||
let maybe_notify = self
|
||||
.fan_control_handle
|
||||
.lock()
|
||||
.map_err(|err| anyhow!("Lock error: {err}"))?
|
||||
.take();
|
||||
if let Some((notify, handle)) = maybe_notify {
|
||||
notify.notify_one();
|
||||
handle.await?;
|
||||
|
||||
if reset_mode {
|
||||
let hw_mon = self
|
||||
.handle
|
||||
.hw_monitors
|
||||
.first()
|
||||
.cloned()
|
||||
.context("This GPU has no monitor")?;
|
||||
hw_mon
|
||||
.set_fan_control_method(FanControlMethod::Auto)
|
||||
.context("Could not set fan control back to automatic")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn apply_config(&self, config: &config::Gpu) -> anyhow::Result<()> {
|
||||
if config.fan_control_enabled {
|
||||
if let Some(ref settings) = config.fan_control_settings {
|
||||
if settings.curve.0.is_empty() {
|
||||
return Err(anyhow!("Cannot use empty fan curve"));
|
||||
}
|
||||
|
||||
let interval = Duration::from_millis(settings.interval_ms);
|
||||
self.start_fan_control(
|
||||
settings.curve.clone(),
|
||||
settings.temperature_key.clone(),
|
||||
interval,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"Trying to enable fan control with no settings provided"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.stop_fan_control(true).await?;
|
||||
}
|
||||
|
||||
if let Some(cap) = config.power_cap {
|
||||
let hw_mon = self.first_hw_mon()?;
|
||||
hw_mon.set_power_cap(cap)?;
|
||||
} else if let Ok(hw_mon) = self.first_hw_mon() {
|
||||
if let Ok(default_cap) = hw_mon.get_power_cap_default() {
|
||||
hw_mon.set_power_cap(default_cap)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = config.performance_level {
|
||||
self.handle.set_power_force_performance_level(level)?;
|
||||
} else if self.handle.get_power_force_performance_level().is_ok() {
|
||||
self.handle
|
||||
.set_power_force_performance_level(PerformanceLevel::Auto)?;
|
||||
}
|
||||
|
||||
if config.max_core_clock.is_some()
|
||||
|| config.max_memory_clock.is_some()
|
||||
|| config.max_voltage.is_some()
|
||||
{
|
||||
let mut table = self.handle.get_clocks_table()?;
|
||||
|
||||
if let Some(clockspeed) = config.max_core_clock {
|
||||
table.set_max_sclk(clockspeed)?;
|
||||
}
|
||||
if let Some(clockspeed) = config.max_memory_clock {
|
||||
table.set_max_mclk(clockspeed)?;
|
||||
}
|
||||
if let Some(voltage) = config.max_voltage {
|
||||
table.set_max_voltage(voltage)?;
|
||||
}
|
||||
|
||||
self.handle
|
||||
.set_clocks_table(&table)
|
||||
.context("Could not write clocks table")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
239
lact-daemon/src/server/handler.rs
Normal file
239
lact-daemon/src/server/handler.rs
Normal file
@ -0,0 +1,239 @@
|
||||
use super::gpu_controller::{fan_control::FanCurve, GpuController};
|
||||
use crate::config::{self, Config, FanControlSettings};
|
||||
use anyhow::{anyhow, Context};
|
||||
use lact_schema::{
|
||||
request::SetClocksCommand, ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanCurveMap,
|
||||
PerformanceLevel,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Handler {
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
pub gpu_controllers: Arc<HashMap<String, GpuController>>,
|
||||
}
|
||||
|
||||
impl<'a> Handler {
|
||||
pub async fn new(config: Config) -> anyhow::Result<Self> {
|
||||
let mut controllers = HashMap::new();
|
||||
|
||||
let base_path = match env::var("_LACT_DRM_SYSFS_PATH") {
|
||||
Ok(custom_path) => PathBuf::from(custom_path),
|
||||
Err(_) => PathBuf::from("/sys/class/drm"),
|
||||
};
|
||||
|
||||
for entry in base_path
|
||||
.read_dir()
|
||||
.map_err(|error| anyhow!("Failed to read sysfs: {error}"))?
|
||||
{
|
||||
let entry = entry?;
|
||||
|
||||
let name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(|_| anyhow!("non-utf path"))?;
|
||||
if name.starts_with("card") && !name.contains('-') {
|
||||
trace!("trying gpu controller at {:?}", entry.path());
|
||||
let device_path = entry.path().join("device");
|
||||
match GpuController::new_from_path(device_path) {
|
||||
Ok(controller) => match controller.get_id() {
|
||||
Ok(id) => {
|
||||
let path = controller.get_path();
|
||||
debug!("initialized GPU controller {id} for path {path:?}",);
|
||||
controllers.insert(id, controller);
|
||||
}
|
||||
Err(err) => warn!("could not initialize controller: {err:#}"),
|
||||
},
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"failed to initialize controller at {:?}, {error}",
|
||||
entry.path()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (id, gpu_config) in &config.gpus {
|
||||
if let Some(controller) = controllers.get(id) {
|
||||
controller.apply_config(gpu_config).await?;
|
||||
} else {
|
||||
info!("could not find GPU with id {id} defined in configuration");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
gpu_controllers: Arc::new(controllers),
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>(
|
||||
&self,
|
||||
id: String,
|
||||
f: F,
|
||||
) -> anyhow::Result<()> {
|
||||
let current_config = self
|
||||
.config
|
||||
.read()
|
||||
.map_err(|err| anyhow!("{err}"))?
|
||||
.gpus
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut new_config = current_config.clone();
|
||||
f(&mut new_config);
|
||||
|
||||
let controller = self.controller_by_id(&id)?;
|
||||
|
||||
match controller.apply_config(&new_config).await {
|
||||
Ok(()) => {
|
||||
let mut config_guard = self.config.write().unwrap();
|
||||
config_guard.gpus.insert(id, new_config);
|
||||
config_guard.save()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(apply_err) => {
|
||||
error!("Could not apply settings: {apply_err:#}");
|
||||
match controller.apply_config(¤t_config).await {
|
||||
Ok(()) => Err(apply_err.context("Could not apply settings")),
|
||||
Err(err) => Err(anyhow!("Could not apply settings, and could not reset to default settings: {err:#}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn controller_by_id(&self, id: &str) -> anyhow::Result<&GpuController> {
|
||||
Ok(self
|
||||
.gpu_controllers
|
||||
.get(id)
|
||||
.as_ref()
|
||||
.context("No controller with such id")?)
|
||||
}
|
||||
|
||||
pub fn list_devices(&'a self) -> Vec<DeviceListEntry<'a>> {
|
||||
self.gpu_controllers
|
||||
.iter()
|
||||
.map(|(id, controller)| {
|
||||
let name = controller
|
||||
.pci_info
|
||||
.as_ref()
|
||||
.and_then(|pci_info| pci_info.device_pci_info.model.as_deref());
|
||||
DeviceListEntry { id, name }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_device_info(&'a self, id: &str) -> anyhow::Result<DeviceInfo<'a>> {
|
||||
Ok(self.controller_by_id(id)?.get_info())
|
||||
}
|
||||
|
||||
pub fn get_gpu_stats(&'a self, id: &str) -> anyhow::Result<DeviceStats> {
|
||||
let config = self
|
||||
.config
|
||||
.read()
|
||||
.map_err(|err| anyhow!("Could not read config: {err:?}"))?;
|
||||
let gpu_config = config.gpus.get(id);
|
||||
self.controller_by_id(id)?.get_stats(gpu_config)
|
||||
}
|
||||
|
||||
pub fn get_clocks_info(&'a self, id: &str) -> anyhow::Result<ClocksInfo> {
|
||||
self.controller_by_id(id)?.get_clocks_info()
|
||||
}
|
||||
|
||||
pub async fn set_fan_control(
|
||||
&'a self,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
curve: Option<FanCurveMap>,
|
||||
) -> anyhow::Result<()> {
|
||||
let settings = match curve {
|
||||
Some(raw_curve) => {
|
||||
let curve = FanCurve(raw_curve);
|
||||
curve.validate()?;
|
||||
|
||||
let mut config_guard = self.config.write().map_err(|err| anyhow!("{err}"))?;
|
||||
let gpu_config = config_guard.gpus.entry(id.to_owned()).or_default();
|
||||
|
||||
if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone() {
|
||||
existing_settings.curve = curve;
|
||||
Some(existing_settings)
|
||||
} else {
|
||||
Some(FanControlSettings {
|
||||
curve,
|
||||
temperature_key: "edge".to_owned(),
|
||||
interval_ms: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.edit_gpu_config(id.to_owned(), |config| {
|
||||
config.fan_control_enabled = enabled;
|
||||
config.fan_control_settings = settings;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_power_cap(&'a self, id: &str, maybe_cap: Option<f64>) -> anyhow::Result<()> {
|
||||
self.edit_gpu_config(id.to_owned(), |gpu_config| {
|
||||
gpu_config.power_cap = maybe_cap;
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_performance_level(
|
||||
&self,
|
||||
id: &str,
|
||||
level: PerformanceLevel,
|
||||
) -> anyhow::Result<()> {
|
||||
self.edit_gpu_config(id.to_owned(), |gpu_config| {
|
||||
gpu_config.performance_level = Some(level);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_clocks_value(
|
||||
&self,
|
||||
id: &str,
|
||||
command: SetClocksCommand,
|
||||
) -> anyhow::Result<()> {
|
||||
if let SetClocksCommand::Reset = command {
|
||||
self.controller_by_id(id)?.handle.reset_clocks_table()?;
|
||||
}
|
||||
|
||||
self.edit_gpu_config(id.to_owned(), |gpu_config| match command {
|
||||
SetClocksCommand::MaxCoreClock(clock) => gpu_config.max_core_clock = Some(clock),
|
||||
SetClocksCommand::MaxMemoryClock(clock) => gpu_config.max_memory_clock = Some(clock),
|
||||
SetClocksCommand::MaxVoltage(voltage) => gpu_config.max_voltage = Some(voltage),
|
||||
SetClocksCommand::Reset => {
|
||||
gpu_config.max_core_clock = None;
|
||||
gpu_config.max_memory_clock = None;
|
||||
gpu_config.max_voltage = None;
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn cleanup(self) {
|
||||
for (id, controller) in self.gpu_controllers.iter() {
|
||||
if controller.handle.get_clocks_table().is_ok() {
|
||||
if let Err(err) = controller.handle.reset_clocks_table() {
|
||||
error!("Could not reset the clocks table: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = controller.apply_config(&config::Gpu::default()).await {
|
||||
error!("Could not reset settings for controller {id}: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
148
lact-daemon/src/server/mod.rs
Normal file
148
lact-daemon/src/server/mod.rs
Normal file
@ -0,0 +1,148 @@
|
||||
pub mod gpu_controller;
|
||||
pub mod handler;
|
||||
// mod pci;
|
||||
mod vulkan;
|
||||
|
||||
use self::handler::Handler;
|
||||
use crate::{config::Config, socket};
|
||||
use anyhow::Context;
|
||||
use lact_schema::{Pong, Request, Response, SystemInfo};
|
||||
use serde::Serialize;
|
||||
use std::{fs, process::Command};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::{UnixListener, UnixStream},
|
||||
};
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
pub struct Server {
|
||||
pub handler: Handler,
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub async fn new(config: Config) -> anyhow::Result<Self> {
|
||||
let listener = socket::listen(&config.daemon.admin_groups)?;
|
||||
let handler = Handler::new(config).await?;
|
||||
|
||||
Ok(Self { handler, listener })
|
||||
}
|
||||
|
||||
pub async fn run(self) {
|
||||
loop {
|
||||
match self.listener.accept().await {
|
||||
Ok((stream, _)) => {
|
||||
let handler = self.handler.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(error) = handle_stream(stream, handler).await {
|
||||
error!("{error}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
error!("failed to handle connection: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(stream, handler))]
|
||||
pub async fn handle_stream(stream: UnixStream, handler: Handler) -> anyhow::Result<()> {
|
||||
let mut stream = BufReader::new(stream);
|
||||
|
||||
let mut buf = String::new();
|
||||
while stream.read_line(&mut buf).await? != 0 {
|
||||
debug!("handling request: {}", buf.trim_end());
|
||||
|
||||
let maybe_request = serde_json::from_str(&buf);
|
||||
let response = match maybe_request {
|
||||
Ok(request) => match handle_request(request, &handler).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => serde_json::to_vec(&Response::<()>::Error(format!("{error:#}")))?,
|
||||
},
|
||||
Err(error) => serde_json::to_vec(&Response::<()>::Error(format!(
|
||||
"Failed to deserialize request: {error}"
|
||||
)))?,
|
||||
};
|
||||
|
||||
stream.write_all(&response).await?;
|
||||
stream.write_all(b"\n").await?;
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(handler))]
|
||||
async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyhow::Result<Vec<u8>> {
|
||||
match request {
|
||||
Request::Ping => ok_response(ping()),
|
||||
Request::SystemInfo => ok_response(system_info()?),
|
||||
Request::ListDevices => ok_response(handler.list_devices()),
|
||||
Request::DeviceInfo { id } => ok_response(handler.get_device_info(id)?),
|
||||
Request::DeviceStats { id } => ok_response(handler.get_gpu_stats(id)?),
|
||||
Request::DeviceClocksInfo { id } => ok_response(handler.get_clocks_info(id)?),
|
||||
Request::SetFanControl { id, enabled, curve } => {
|
||||
ok_response(handler.set_fan_control(id, enabled, curve).await?)
|
||||
}
|
||||
Request::SetPowerCap { id, cap } => ok_response(handler.set_power_cap(id, cap).await?),
|
||||
Request::SetPerformanceLevel {
|
||||
id,
|
||||
performance_level,
|
||||
} => ok_response(handler.set_performance_level(id, performance_level).await?),
|
||||
Request::SetClocksValue { id, command } => {
|
||||
ok_response(handler.set_clocks_value(id, command).await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ok_response<T: Serialize>(data: T) -> anyhow::Result<Vec<u8>> {
|
||||
Ok(serde_json::to_vec(&Response::Ok(data))?)
|
||||
}
|
||||
|
||||
fn ping() -> Pong {
|
||||
Pong
|
||||
}
|
||||
|
||||
fn system_info() -> anyhow::Result<SystemInfo<'static>> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let profile = if cfg!(debug_assertions) {
|
||||
"debug"
|
||||
} else {
|
||||
"release"
|
||||
};
|
||||
let kernel_output = Command::new("uname")
|
||||
.arg("-r")
|
||||
.output()
|
||||
.context("Could not read kernel version")?;
|
||||
let kernel_version = String::from_utf8(kernel_output.stdout)
|
||||
.context("Invalid kernel version output")?
|
||||
.trim()
|
||||
.to_owned();
|
||||
|
||||
let amdgpu_overdrive_enabled = if let Ok(ppfeaturemask) =
|
||||
fs::read_to_string("/sys/module/amdgpu/parameters/ppfeaturemask")
|
||||
{
|
||||
const PP_OVERDRIVE_MASK: i32 = 0x4000;
|
||||
|
||||
let ppfeaturemask = ppfeaturemask
|
||||
.trim()
|
||||
.strip_prefix("0x")
|
||||
.context("Invalid ppfeaturemask")?;
|
||||
let ppfeaturemask: u64 =
|
||||
u64::from_str_radix(ppfeaturemask, 16).context("Invalid ppfeaturemask")?;
|
||||
|
||||
Some((ppfeaturemask & PP_OVERDRIVE_MASK as u64) > 0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(SystemInfo {
|
||||
version,
|
||||
profile,
|
||||
kernel_version,
|
||||
amdgpu_overdrive_enabled,
|
||||
})
|
||||
}
|
55
lact-daemon/src/server/vulkan.rs
Normal file
55
lact-daemon/src/server/vulkan.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::fork::run_forked;
|
||||
use lact_schema::{VulkanDriverInfo, VulkanInfo};
|
||||
use vulkano::{
|
||||
instance::{Instance, InstanceCreateInfo},
|
||||
VulkanLibrary,
|
||||
};
|
||||
|
||||
pub fn get_vulkan_info<'a>(vendor_id: &'a str, device_id: &'a str) -> anyhow::Result<VulkanInfo> {
|
||||
let vendor_id = u32::from_str_radix(vendor_id, 16)?;
|
||||
let device_id = u32::from_str_radix(device_id, 16)?;
|
||||
|
||||
unsafe {
|
||||
run_forked(|| {
|
||||
let library = VulkanLibrary::new().map_err(|err| err.to_string())?;
|
||||
let instance = Instance::new(library, InstanceCreateInfo::default())
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enabled_layers = instance.enabled_layers().to_vec();
|
||||
let devices = instance
|
||||
.enumerate_physical_devices()
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
for device in devices {
|
||||
let properties = device.properties();
|
||||
// Not sure how this works with systems that have multiple identical GPUs
|
||||
if (properties.vendor_id, properties.device_id) == (vendor_id, device_id) {
|
||||
let info = VulkanInfo {
|
||||
device_name: properties.device_name.clone(),
|
||||
api_version: device.api_version().to_string(),
|
||||
driver: VulkanDriverInfo {
|
||||
version: properties.driver_version,
|
||||
name: properties.driver_name.clone(),
|
||||
info: properties.driver_info.clone(),
|
||||
},
|
||||
features: device
|
||||
.supported_features()
|
||||
.into_iter()
|
||||
.map(|(name, enabled)| (Cow::Borrowed(name), enabled))
|
||||
.collect(),
|
||||
extensions: device
|
||||
.supported_extensions()
|
||||
.into_iter()
|
||||
.map(|(name, enabled)| (Cow::Borrowed(name), enabled))
|
||||
.collect(),
|
||||
enabled_layers,
|
||||
};
|
||||
return Ok(info);
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not find a vulkan device with matching pci ids".to_owned())
|
||||
})
|
||||
}
|
||||
}
|
67
lact-daemon/src/socket.rs
Normal file
67
lact-daemon/src/socket.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use anyhow::anyhow;
|
||||
use nix::{
|
||||
sys::stat::{umask, Mode},
|
||||
unistd::{chown, getuid, Gid, Group},
|
||||
};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
use tokio::net::UnixListener;
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub fn get_socket_path() -> PathBuf {
|
||||
let uid = getuid();
|
||||
if uid.is_root() {
|
||||
PathBuf::from_str("/var/run/lactd.sock").unwrap()
|
||||
} else {
|
||||
PathBuf::from_str(&format!("/var/run/user/{uid}/lactd.sock")).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup() {
|
||||
let socket_path = get_socket_path();
|
||||
|
||||
if socket_path.exists() {
|
||||
fs::remove_file(socket_path).expect("failed to remove socket");
|
||||
}
|
||||
debug!("removed socket");
|
||||
}
|
||||
|
||||
pub fn listen(admin_groups: &[String]) -> anyhow::Result<UnixListener> {
|
||||
let socket_path = get_socket_path();
|
||||
|
||||
if socket_path.exists() {
|
||||
return Err(anyhow!(
|
||||
"Socket {socket_path:?} already exists. \
|
||||
This probably means that another instance of lact-daemon is currently running. \
|
||||
If you are sure that this is not the case, please remove the file"
|
||||
));
|
||||
}
|
||||
|
||||
let socket_mask = Mode::S_IXUSR | Mode::S_IXGRP | Mode::S_IRWXO;
|
||||
umask(socket_mask);
|
||||
|
||||
let listener = UnixListener::bind(&socket_path)?;
|
||||
|
||||
chown(&socket_path, None, Some(socket_gid(admin_groups)))?;
|
||||
|
||||
info!("listening on {socket_path:?}");
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
fn socket_gid(admin_groups: &[String]) -> Gid {
|
||||
if getuid().is_root() {
|
||||
// Check if the group exists
|
||||
for group_name in admin_groups {
|
||||
if let Ok(Some(group)) = Group::from_name(group_name) {
|
||||
return group.gid;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(Some(group)) = Group::from_gid(Gid::from_raw(1000)) {
|
||||
group.gid
|
||||
} else {
|
||||
Gid::current()
|
||||
}
|
||||
} else {
|
||||
Gid::current()
|
||||
}
|
||||
}
|
21
lact-gui/Cargo.toml
Normal file
21
lact-gui/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "lact-gui"
|
||||
version = "0.2.0"
|
||||
authors = ["Ilya Zlobintsev <ilya.zl@protonmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["gtk-tests"]
|
||||
gtk-tests = []
|
||||
|
||||
[dependencies]
|
||||
lact-client = { path = "../lact-client" }
|
||||
lact-daemon = { path = "../lact-daemon" }
|
||||
gtk = { version = "0.6", package = "gtk4", features = ["v4_6"] }
|
||||
once_cell = "1.17.1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
anyhow = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.3.0"
|
50
lact-gui/src/app/apply_revealer.rs
Normal file
50
lact-gui/src/app/apply_revealer.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApplyRevealer {
|
||||
pub container: Revealer,
|
||||
apply_button: Button,
|
||||
reset_button: Button,
|
||||
}
|
||||
|
||||
impl ApplyRevealer {
|
||||
pub fn new() -> Self {
|
||||
let container = Revealer::builder().transition_duration(150).build();
|
||||
let vbox = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let apply_button = Button::builder().label("Apply").hexpand(true).build();
|
||||
let reset_button = Button::builder().label("Reset").build();
|
||||
|
||||
vbox.append(&apply_button);
|
||||
vbox.append(&reset_button);
|
||||
|
||||
container.set_child(Some(&vbox));
|
||||
|
||||
Self {
|
||||
container,
|
||||
apply_button,
|
||||
reset_button,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_reveal_child(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_reveal_child(false);
|
||||
}
|
||||
|
||||
pub fn connect_apply_button_clicked<F: Fn() + 'static>(&self, f: F) {
|
||||
self.apply_button.connect_clicked(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connect_reset_button_clicked<F: Fn() + 'static>(&self, f: F) {
|
||||
self.reset_button.connect_clicked(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::DeviceListEntry;
|
||||
use pango::EllipsizeMode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Header {
|
||||
@ -14,9 +14,10 @@ impl Header {
|
||||
pub fn new() -> Self {
|
||||
let container = HeaderBar::new();
|
||||
|
||||
container.set_custom_title(Some(&Grid::new())); // Bad workaround to hide the title
|
||||
// TODO Check if this is this still needed
|
||||
container.set_title_widget(Some(&Grid::new())); // Bad workaround to hide the title
|
||||
|
||||
container.set_show_close_button(true);
|
||||
container.set_show_title_buttons(true);
|
||||
|
||||
let gpu_selector = ComboBoxText::new();
|
||||
container.pack_start(&gpu_selector);
|
||||
@ -35,25 +36,26 @@ impl Header {
|
||||
self.switcher.set_stack(Some(stack));
|
||||
}
|
||||
|
||||
pub fn set_gpus(&self, gpus: HashMap<u32, Option<String>>) {
|
||||
for (id, name) in &gpus {
|
||||
pub fn set_devices(&self, gpus: &[DeviceListEntry<'_>]) {
|
||||
for entry in gpus {
|
||||
self.gpu_selector
|
||||
.append(Some(&id.to_string()), &name.clone().unwrap_or_default());
|
||||
.append(Some(entry.id), entry.name.unwrap_or_default());
|
||||
}
|
||||
|
||||
//limits the length of gpu names in combobox
|
||||
for cell in self.gpu_selector.cells() {
|
||||
cell.set_property("width-chars", &10).unwrap();
|
||||
cell.set_property("ellipsize", &EllipsizeMode::End).unwrap();
|
||||
cell.set_property("width-chars", &10);
|
||||
cell.set_property("ellipsize", &EllipsizeMode::End);
|
||||
}
|
||||
|
||||
self.gpu_selector.set_active(Some(0));
|
||||
}
|
||||
|
||||
pub fn connect_gpu_selection_changed<F: Fn(u32) + 'static>(&self, f: F) {
|
||||
pub fn connect_gpu_selection_changed<F: Fn(String) + 'static>(&self, f: F) {
|
||||
self.gpu_selector.connect_changed(move |gpu_selector| {
|
||||
let selected_id = gpu_selector.active_id().unwrap();
|
||||
f(selected_id.parse().unwrap());
|
||||
if let Some(selected_id) = gpu_selector.active_id() {
|
||||
f(selected_id.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
321
lact-gui/src/app/mod.rs
Normal file
321
lact-gui/src/app/mod.rs
Normal file
@ -0,0 +1,321 @@
|
||||
mod apply_revealer;
|
||||
mod header;
|
||||
mod root_stack;
|
||||
|
||||
use crate::APP_ID;
|
||||
use anyhow::{anyhow, Context};
|
||||
use apply_revealer::ApplyRevealer;
|
||||
use glib::clone;
|
||||
use gtk::{gio::ApplicationFlags, prelude::*, *};
|
||||
use header::Header;
|
||||
use lact_client::schema::request::SetClocksCommand;
|
||||
use lact_client::schema::DeviceStats;
|
||||
use lact_client::DaemonClient;
|
||||
use root_stack::RootStack;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
// In ms
|
||||
const STATS_POLL_INTERVAL: u64 = 250;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct App {
|
||||
application: Application,
|
||||
pub window: ApplicationWindow,
|
||||
pub header: Header,
|
||||
root_stack: RootStack,
|
||||
apply_revealer: ApplyRevealer,
|
||||
daemon_client: DaemonClient,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(daemon_client: DaemonClient) -> Self {
|
||||
let application = Application::new(Some(APP_ID), ApplicationFlags::default());
|
||||
|
||||
let header = Header::new();
|
||||
let window = ApplicationWindow::builder()
|
||||
.title("LACT")
|
||||
.default_width(500)
|
||||
.default_height(600)
|
||||
.icon_name(APP_ID)
|
||||
.build();
|
||||
|
||||
window.set_titlebar(Some(&header.container));
|
||||
|
||||
let system_info_buf = daemon_client
|
||||
.get_system_info()
|
||||
.expect("Could not fetch system info");
|
||||
let system_info = system_info_buf.inner().expect("Invalid system info buffer");
|
||||
let root_stack = RootStack::new(system_info, daemon_client.embedded);
|
||||
|
||||
header.set_switcher_stack(&root_stack.container);
|
||||
|
||||
let root_box = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
root_box.append(&root_stack.container);
|
||||
|
||||
let apply_revealer = ApplyRevealer::new();
|
||||
|
||||
root_box.append(&apply_revealer.container);
|
||||
|
||||
window.set_child(Some(&root_box));
|
||||
|
||||
App {
|
||||
application,
|
||||
window,
|
||||
header,
|
||||
root_stack,
|
||||
apply_revealer,
|
||||
daemon_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
self.application
|
||||
.connect_activate(clone!(@strong self as app => move |_| {
|
||||
app.window.set_application(Some(&app.application));
|
||||
|
||||
let current_gpu_id = Arc::new(RwLock::new(String::new()));
|
||||
|
||||
|
||||
app.header.connect_gpu_selection_changed(clone!(@strong app, @strong current_gpu_id => move |gpu_id| {
|
||||
debug!("GPU Selection changed");
|
||||
app.set_info(&gpu_id);
|
||||
*current_gpu_id.write().unwrap() = gpu_id;
|
||||
}));
|
||||
|
||||
let devices_buf = app
|
||||
.daemon_client
|
||||
.list_devices()
|
||||
.expect("Could not list devices");
|
||||
let devices = devices_buf.inner().expect("Could not access devices");
|
||||
app.header.set_devices(&devices);
|
||||
|
||||
|
||||
app.root_stack.oc_page.clocks_frame.connect_clocks_reset(clone!(@strong app, @strong current_gpu_id => move || {
|
||||
debug!("Resetting clocks");
|
||||
|
||||
let gpu_id = current_gpu_id.read().unwrap();
|
||||
|
||||
match app.daemon_client.set_clocks_value(&gpu_id, SetClocksCommand::Reset) {
|
||||
Ok(()) => {
|
||||
app.set_initial(&gpu_id);
|
||||
}
|
||||
Err(err) => {
|
||||
show_error(&app.window, err);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
app.apply_revealer.connect_apply_button_clicked(
|
||||
clone!(@strong app, @strong current_gpu_id => move || {
|
||||
if let Err(err) = app.apply_settings(current_gpu_id.clone()) {
|
||||
show_error(&app.window, err.context("Could not apply settings"));
|
||||
|
||||
glib::idle_add_local_once(clone!(@strong app, @strong current_gpu_id => move || {
|
||||
let gpu_id = current_gpu_id.read().unwrap();
|
||||
app.set_initial(&gpu_id)
|
||||
}));
|
||||
}
|
||||
}),
|
||||
);
|
||||
app.apply_revealer.connect_reset_button_clicked(clone!(@strong app, @strong current_gpu_id => move || {
|
||||
let gpu_id = current_gpu_id.read().unwrap();
|
||||
app.set_initial(&gpu_id)
|
||||
}));
|
||||
|
||||
app.start_stats_update_loop(current_gpu_id);
|
||||
|
||||
app.window.show();
|
||||
|
||||
if app.daemon_client.embedded {
|
||||
show_error(&app.window, anyhow!(
|
||||
"Could not connect to daemon, running in embedded mode. \n\
|
||||
Please make sure the lactd service is running. \n\
|
||||
Using embedded mode, you will not be able to change any settings."
|
||||
));
|
||||
}
|
||||
}));
|
||||
|
||||
// Args are passed manually since they were already processed by clap before
|
||||
self.application.run_with_args::<String>(&[]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_info(&self, gpu_id: &str) {
|
||||
let info_buf = self
|
||||
.daemon_client
|
||||
.get_device_info(gpu_id)
|
||||
.expect("Could not fetch info");
|
||||
let info = info_buf.inner().unwrap();
|
||||
|
||||
trace!("setting info {info:?}");
|
||||
|
||||
self.root_stack.info_page.set_info(&info);
|
||||
|
||||
self.set_initial(gpu_id);
|
||||
}
|
||||
|
||||
fn set_initial(&self, gpu_id: &str) {
|
||||
let stats_buf = self
|
||||
.daemon_client
|
||||
.get_device_stats(gpu_id)
|
||||
.expect("Could not fetch stats");
|
||||
let stats = stats_buf.inner().unwrap();
|
||||
|
||||
self.root_stack.oc_page.set_stats(&stats, true);
|
||||
self.root_stack.thermals_page.set_stats(&stats, true);
|
||||
|
||||
let maybe_clocks_table = match self.daemon_client.get_device_clocks_info(gpu_id) {
|
||||
Ok(clocks_buf) => match clocks_buf.inner() {
|
||||
Ok(info) => info.table,
|
||||
Err(err) => {
|
||||
debug!("could not extract clocks info: {err:?}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
debug!("could not fetch clocks info: {err:?}");
|
||||
None
|
||||
}
|
||||
};
|
||||
self.root_stack.oc_page.set_clocks_table(maybe_clocks_table);
|
||||
|
||||
// Show apply button on setting changes
|
||||
// This is done here because new widgets may appear after applying settings (like fan curve points) which should be connected
|
||||
let show_revealer = clone!(@strong self.apply_revealer as apply_revealer => move || {
|
||||
debug!("settings changed, showing apply button");
|
||||
apply_revealer.show();
|
||||
});
|
||||
|
||||
self.root_stack
|
||||
.thermals_page
|
||||
.connect_settings_changed(show_revealer.clone());
|
||||
|
||||
self.root_stack
|
||||
.oc_page
|
||||
.connect_settings_changed(show_revealer);
|
||||
|
||||
self.apply_revealer.hide();
|
||||
}
|
||||
|
||||
fn start_stats_update_loop(&self, current_gpu_id: Arc<RwLock<String>>) {
|
||||
let context = glib::MainContext::default();
|
||||
|
||||
let _guard = context.acquire();
|
||||
|
||||
// The loop that gets stats
|
||||
let (sender, receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||
|
||||
thread::spawn(
|
||||
clone!(@strong self.daemon_client as daemon_client => move || loop {
|
||||
let gpu_id = current_gpu_id.read().unwrap();
|
||||
match daemon_client
|
||||
.get_device_stats(&gpu_id)
|
||||
.and_then(|stats| stats.inner())
|
||||
{
|
||||
Ok(stats) => {
|
||||
sender.send(GuiUpdateMsg::GpuStats(stats)).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Could not fetch stats: {err}");
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(STATS_POLL_INTERVAL));
|
||||
}),
|
||||
);
|
||||
|
||||
// Receiving stats into the gui event loop
|
||||
|
||||
receiver.attach(
|
||||
None,
|
||||
clone!(@strong self.root_stack as root_stack => move |msg| {
|
||||
match msg {
|
||||
GuiUpdateMsg::GpuStats(stats) => {
|
||||
trace!("new stats received, updating {stats:?}");
|
||||
root_stack.info_page.set_stats(&stats);
|
||||
root_stack.thermals_page.set_stats(&stats, false);
|
||||
root_stack.oc_page.set_stats(&stats, false);
|
||||
}
|
||||
}
|
||||
|
||||
glib::Continue(true)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_settings(&self, current_gpu_id: Arc<RwLock<String>>) -> anyhow::Result<()> {
|
||||
debug!("applying settings");
|
||||
|
||||
let gpu_id = current_gpu_id.read().unwrap();
|
||||
|
||||
if let Some(cap) = self.root_stack.oc_page.get_power_cap() {
|
||||
self.daemon_client
|
||||
.set_power_cap(&gpu_id, Some(cap))
|
||||
.context("Failed to set power cap")?;
|
||||
}
|
||||
|
||||
if let Some(level) = self.root_stack.oc_page.get_performance_level() {
|
||||
self.daemon_client
|
||||
.set_performance_level(&gpu_id, level)
|
||||
.context("Failed to set power profile")?;
|
||||
}
|
||||
|
||||
if let Some(thermals_settings) = self.root_stack.thermals_page.get_thermals_settings() {
|
||||
debug!("applying thermal settings: {thermals_settings:?}");
|
||||
|
||||
self.daemon_client
|
||||
.set_fan_control(
|
||||
&gpu_id,
|
||||
thermals_settings.manual_fan_control,
|
||||
thermals_settings.curve,
|
||||
)
|
||||
.context("Could not set fan control")?;
|
||||
}
|
||||
|
||||
let clocks_settings = self.root_stack.oc_page.clocks_frame.get_settings();
|
||||
|
||||
if let Some(clock) = clocks_settings.max_core_clock {
|
||||
self.daemon_client
|
||||
.set_clocks_value(&gpu_id, SetClocksCommand::MaxCoreClock(clock))
|
||||
.context("Could not set the maximum core clock")?;
|
||||
}
|
||||
|
||||
if let Some(clock) = clocks_settings.max_memory_clock {
|
||||
self.daemon_client
|
||||
.set_clocks_value(&gpu_id, SetClocksCommand::MaxMemoryClock(clock))
|
||||
.context("Could not set the maximum memory clock")?;
|
||||
}
|
||||
|
||||
if let Some(voltage) = clocks_settings.max_voltage {
|
||||
self.daemon_client
|
||||
.set_clocks_value(&gpu_id, SetClocksCommand::MaxVoltage(voltage))
|
||||
.context("Could not set the maximum voltage")?;
|
||||
}
|
||||
|
||||
self.set_initial(&gpu_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum GuiUpdateMsg {
|
||||
GpuStats(DeviceStats),
|
||||
}
|
||||
|
||||
fn show_error(parent: &ApplicationWindow, err: anyhow::Error) {
|
||||
let text = format!("{err:?}");
|
||||
warn!("{}", text.trim());
|
||||
let diag = MessageDialog::builder()
|
||||
.title("Error")
|
||||
.message_type(MessageType::Error)
|
||||
.text(&text)
|
||||
.buttons(ButtonsType::Close)
|
||||
.transient_for(parent)
|
||||
.build();
|
||||
diag.run_async(|diag, _| {
|
||||
diag.hide();
|
||||
})
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
mod vulkan_info;
|
||||
|
||||
use daemon::gpu_controller::GpuInfo;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{DeviceInfo, DeviceStats};
|
||||
use vulkan_info::VulkanInfoFrame;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -31,6 +31,13 @@ impl InformationPage {
|
||||
container.set_row_spacing(7);
|
||||
container.set_column_spacing(5);
|
||||
|
||||
// Dummy label to prevent the gpu name label from stealing focus
|
||||
let dummy_label = Label::builder()
|
||||
.selectable(true)
|
||||
.halign(Align::Start)
|
||||
.build();
|
||||
container.attach(&dummy_label, 0, 0, 1, 1);
|
||||
|
||||
container.attach(
|
||||
&{
|
||||
let label = Label::new(Some("GPU Model:"));
|
||||
@ -43,9 +50,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let gpu_name_label = Label::new(None);
|
||||
gpu_name_label.set_halign(Align::Start);
|
||||
|
||||
let gpu_name_label = value_label();
|
||||
container.attach(&gpu_name_label, 2, 0, 3, 1);
|
||||
|
||||
container.attach(
|
||||
@ -60,9 +65,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let gpu_manufacturer_label = Label::new(None);
|
||||
gpu_manufacturer_label.set_halign(Align::Start);
|
||||
|
||||
let gpu_manufacturer_label = value_label();
|
||||
container.attach(&gpu_manufacturer_label, 2, 1, 3, 1);
|
||||
|
||||
container.attach(
|
||||
@ -77,9 +80,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let vbios_version_label = Label::new(None);
|
||||
vbios_version_label.set_halign(Align::Start);
|
||||
|
||||
let vbios_version_label = value_label();
|
||||
container.attach(&vbios_version_label, 2, 2, 3, 1);
|
||||
|
||||
container.attach(
|
||||
@ -94,9 +95,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let driver_label = Label::new(None);
|
||||
driver_label.set_halign(Align::Start);
|
||||
|
||||
let driver_label = value_label();
|
||||
container.attach(&driver_label, 2, 3, 3, 1);
|
||||
|
||||
container.attach(
|
||||
@ -111,9 +110,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let vram_size_label = Label::new(None);
|
||||
vram_size_label.set_halign(Align::Start);
|
||||
|
||||
let vram_size_label = value_label();
|
||||
container.attach(&vram_size_label, 2, 4, 3, 1);
|
||||
|
||||
container.attach(
|
||||
@ -128,7 +125,7 @@ impl InformationPage {
|
||||
1,
|
||||
);
|
||||
|
||||
let link_speed_label = Label::new(None);
|
||||
let link_speed_label = value_label();
|
||||
link_speed_label.set_halign(Align::Start);
|
||||
|
||||
container.attach(&link_speed_label, 2, 5, 3, 1);
|
||||
@ -148,31 +145,73 @@ impl InformationPage {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_info(&self, gpu_info: &GpuInfo) {
|
||||
self.gpu_name_label.set_markup(&format!(
|
||||
"<b>{}</b>",
|
||||
match &gpu_info.vendor_data.card_model {
|
||||
Some(card_model) => card_model.clone(),
|
||||
None => gpu_info.vendor_data.gpu_model.clone().unwrap_or_default(),
|
||||
}
|
||||
));
|
||||
self.gpu_manufacturer_label.set_markup(&format!(
|
||||
"<b>{}</b>",
|
||||
gpu_info.vendor_data.card_vendor.clone().unwrap_or_default()
|
||||
));
|
||||
pub fn set_info(&self, gpu_info: &DeviceInfo) {
|
||||
let gpu_name = gpu_info
|
||||
.pci_info
|
||||
.as_ref()
|
||||
.and_then(|pci_info| {
|
||||
pci_info
|
||||
.subsystem_pci_info
|
||||
.model
|
||||
.as_deref()
|
||||
.or(pci_info.device_pci_info.model.as_deref())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.gpu_name_label
|
||||
.set_markup(&format!("<b>{gpu_name}</b>",));
|
||||
|
||||
let gpu_manufacturer = gpu_info
|
||||
.pci_info
|
||||
.as_ref()
|
||||
.and_then(|pci_info| {
|
||||
pci_info
|
||||
.subsystem_pci_info
|
||||
.vendor
|
||||
.as_deref()
|
||||
.or(pci_info.device_pci_info.model.as_deref())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.gpu_manufacturer_label
|
||||
.set_markup(&format!("<b>{gpu_manufacturer}</b>",));
|
||||
|
||||
let vbios_version = gpu_info.vbios_version.as_deref().unwrap_or("Unknown");
|
||||
self.vbios_version_label
|
||||
.set_markup(&format!("<b>{}</b>", gpu_info.vbios_version));
|
||||
.set_markup(&format!("<b>{vbios_version}</b>",));
|
||||
|
||||
self.driver_label
|
||||
.set_markup(&format!("<b>{}</b>", gpu_info.driver));
|
||||
|
||||
let link_speed = gpu_info
|
||||
.link_info
|
||||
.current_speed
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown");
|
||||
let link_width = gpu_info
|
||||
.link_info
|
||||
.current_width
|
||||
.as_deref()
|
||||
.unwrap_or("Unknown");
|
||||
self.link_speed_label
|
||||
.set_markup(&format!("<b>{link_speed} x{link_width}</b>",));
|
||||
|
||||
if let Some(vulkan_info) = &gpu_info.vulkan_info {
|
||||
self.vulkan_info_frame.set_info(vulkan_info);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &DeviceStats) {
|
||||
let vram_size = stats.vram.total.map_or_else(
|
||||
|| "Unknown".to_owned(),
|
||||
|size| (size / 1024 / 1024).to_string(),
|
||||
);
|
||||
self.vram_size_label
|
||||
.set_markup(&format!("<b>{}</b>", gpu_info.vram_size));
|
||||
self.link_speed_label.set_markup(&format!(
|
||||
"<b>{} x{}</b>",
|
||||
gpu_info.link_speed, gpu_info.link_width
|
||||
));
|
||||
|
||||
self.vulkan_info_frame.set_info(&gpu_info.vulkan_info);
|
||||
|
||||
self.container.show_all();
|
||||
.set_markup(&format!("<b>{vram_size} MiB</b>"));
|
||||
}
|
||||
}
|
||||
|
||||
fn value_label() -> Label {
|
||||
Label::builder()
|
||||
.selectable(true)
|
||||
.halign(Align::Start)
|
||||
.build()
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
use gio::subclass::prelude::*;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, ParamSpec, ParamSpecBoolean, ParamSpecString},
|
||||
prelude::*,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeatureModel {
|
||||
pub name: RefCell<String>,
|
||||
pub supported: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for FeatureModel {
|
||||
const NAME: &'static str = "VulkanFeatureModel";
|
||||
type Type = super::FeatureModel;
|
||||
}
|
||||
|
||||
impl ObjectImpl for FeatureModel {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
ParamSpecString::builder("name").build(),
|
||||
ParamSpecBoolean::builder("supported").build(),
|
||||
]
|
||||
});
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &ParamSpec) {
|
||||
match pspec.name() {
|
||||
"name" => {
|
||||
let name = value.get().expect("Name needs to be a string");
|
||||
self.name.replace(name);
|
||||
}
|
||||
"supported" => {
|
||||
let supported = value.get().expect("Supported needs to be a bool");
|
||||
self.supported.replace(supported);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, _id: usize, pspec: &ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"name" => self.name.borrow().to_value(),
|
||||
"supported" => self.supported.get().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
mod imp;
|
||||
|
||||
use gtk::glib::{self, Object};
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct FeatureModel(ObjectSubclass<imp::FeatureModel>);
|
||||
}
|
||||
|
||||
impl FeatureModel {
|
||||
pub fn new(name: String, supported: bool) -> Self {
|
||||
Object::builder()
|
||||
.property("name", name)
|
||||
.property("supported", supported)
|
||||
.build()
|
||||
}
|
||||
}
|
237
lact-gui/src/app/root_stack/info_page/vulkan_info/mod.rs
Normal file
237
lact-gui/src/app/root_stack/info_page/vulkan_info/mod.rs
Normal file
@ -0,0 +1,237 @@
|
||||
mod feature_model;
|
||||
|
||||
use self::feature_model::FeatureModel;
|
||||
use super::value_label;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::VulkanInfo;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tracing::trace;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VulkanInfoFrame {
|
||||
pub container: Frame,
|
||||
device_name_label: Label,
|
||||
version_label: Label,
|
||||
features: Rc<RefCell<Vec<FeatureModel>>>,
|
||||
extensions: Rc<RefCell<Vec<FeatureModel>>>,
|
||||
}
|
||||
|
||||
impl VulkanInfoFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = Frame::new(None);
|
||||
|
||||
container.set_label_widget(Some(&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("<span font_desc='11'><b>Vulkan Information</b></span>");
|
||||
label
|
||||
}));
|
||||
container.set_label_align(0.5);
|
||||
|
||||
let features = Rc::new(RefCell::new(Vec::new()));
|
||||
let extensions = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let vbox = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
let grid = Grid::new();
|
||||
|
||||
grid.set_margin_start(5);
|
||||
grid.set_margin_end(5);
|
||||
grid.set_margin_bottom(5);
|
||||
grid.set_margin_top(5);
|
||||
|
||||
grid.set_column_homogeneous(true);
|
||||
grid.set_row_homogeneous(false);
|
||||
|
||||
grid.set_row_spacing(7);
|
||||
grid.set_column_spacing(5);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Device name:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
|
||||
let device_name_label = value_label();
|
||||
grid.attach(&device_name_label, 2, 0, 3, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Version:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
);
|
||||
|
||||
let version_label = value_label();
|
||||
grid.attach(&version_label, 2, 1, 3, 1);
|
||||
|
||||
let features_label = Label::builder()
|
||||
.label("Features:")
|
||||
.halign(Align::End)
|
||||
.build();
|
||||
let show_features_button = Button::builder().label("Show").halign(Align::Start).build();
|
||||
show_features_button.connect_clicked(clone!(@strong features => move |_| {
|
||||
show_list_window("Vulkan features", &features.borrow());
|
||||
}));
|
||||
|
||||
grid.attach(&features_label, 0, 2, 2, 1);
|
||||
grid.attach(&show_features_button, 2, 2, 2, 1);
|
||||
|
||||
let extensions_label = Label::builder()
|
||||
.label("Extensions:")
|
||||
.halign(Align::End)
|
||||
.build();
|
||||
let show_extensions_button = Button::builder().label("Show").halign(Align::Start).build();
|
||||
show_extensions_button.connect_clicked(clone!(@strong extensions => move |_| {
|
||||
show_list_window("Vulkan extensions", &extensions.borrow());
|
||||
}));
|
||||
|
||||
grid.attach(&extensions_label, 0, 3, 2, 1);
|
||||
grid.attach(&show_extensions_button, 2, 3, 2, 1);
|
||||
|
||||
vbox.prepend(&grid);
|
||||
|
||||
container.set_child(Some(&vbox));
|
||||
|
||||
Self {
|
||||
container,
|
||||
device_name_label,
|
||||
version_label,
|
||||
features,
|
||||
extensions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_info(&self, vulkan_info: &VulkanInfo) {
|
||||
trace!("setting vulkan info: {:?}", vulkan_info);
|
||||
|
||||
self.device_name_label
|
||||
.set_markup(&format!("<b>{}</b>", vulkan_info.device_name));
|
||||
self.version_label
|
||||
.set_markup(&format!("<b>{}</b>", vulkan_info.api_version));
|
||||
|
||||
let features_vec: Vec<_> = vulkan_info
|
||||
.features
|
||||
.iter()
|
||||
.map(|(name, supported)| FeatureModel::new(name.to_string(), *supported))
|
||||
.collect();
|
||||
self.features.replace(features_vec);
|
||||
|
||||
let extensions_vec: Vec<_> = vulkan_info
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|(name, supported)| FeatureModel::new(name.to_string(), *supported))
|
||||
.collect();
|
||||
self.extensions.replace(extensions_vec);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_list_window(title: &str, items: &[FeatureModel]) {
|
||||
let window = Window::builder()
|
||||
.title(title)
|
||||
.width_request(500)
|
||||
.height_request(700)
|
||||
.build();
|
||||
|
||||
let base_model = gio::ListStore::new(FeatureModel::static_type());
|
||||
base_model.extend_from_slice(items);
|
||||
|
||||
let expression = PropertyExpression::new(FeatureModel::static_type(), Expression::NONE, "name");
|
||||
let filter = StringFilter::builder()
|
||||
.match_mode(StringFilterMatchMode::Substring)
|
||||
.ignore_case(true)
|
||||
.expression(expression)
|
||||
.build();
|
||||
|
||||
let entry = SearchEntry::builder().hexpand(true).build();
|
||||
entry.connect_search_changed(clone!(@weak filter => move |entry| {
|
||||
if entry.text().is_empty() {
|
||||
filter.set_search(None);
|
||||
} else {
|
||||
filter.set_search(Some(entry.text().as_str()));
|
||||
}
|
||||
}));
|
||||
let search_bar = SearchBar::builder()
|
||||
.child(&entry)
|
||||
.search_mode_enabled(true)
|
||||
.key_capture_widget(&window)
|
||||
.build();
|
||||
|
||||
let filter_model = FilterListModel::builder()
|
||||
.model(&base_model)
|
||||
.filter(&filter)
|
||||
.incremental(true)
|
||||
.build();
|
||||
|
||||
let selection_model = NoSelection::new(Some(filter_model));
|
||||
|
||||
let factory = gtk::SignalListItemFactory::new();
|
||||
|
||||
factory.connect_setup(move |_factory, item| {
|
||||
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let label = Label::builder()
|
||||
.margin_top(5)
|
||||
.margin_bottom(5)
|
||||
.selectable(true)
|
||||
.hexpand(true)
|
||||
.halign(Align::Start)
|
||||
.build();
|
||||
let image = Image::new();
|
||||
let vbox = Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(5)
|
||||
.margin_start(10)
|
||||
.margin_end(10)
|
||||
.build();
|
||||
vbox.append(&label);
|
||||
vbox.append(&image);
|
||||
item.set_child(Some(&vbox));
|
||||
});
|
||||
|
||||
factory.connect_bind(move |_factory, item| {
|
||||
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
|
||||
let model = item.item().and_downcast::<FeatureModel>().unwrap();
|
||||
|
||||
let vbox = item.child().and_downcast::<Box>().unwrap();
|
||||
let children = vbox.observe_children();
|
||||
let label = children.item(0).and_downcast::<Label>().unwrap();
|
||||
let image = children.item(1).and_downcast::<Image>().unwrap();
|
||||
|
||||
let text = model.property::<String>("name");
|
||||
let supported = model.property::<bool>("supported");
|
||||
label.set_text(&text);
|
||||
|
||||
let icon_name = if supported {
|
||||
"emblem-ok-symbolic"
|
||||
} else {
|
||||
"action-unavailable-symbolic"
|
||||
};
|
||||
image.set_icon_name(Some(icon_name));
|
||||
});
|
||||
|
||||
let list_view = ListView::new(Some(selection_model), Some(factory));
|
||||
let scroll_window = ScrolledWindow::builder()
|
||||
.child(&list_view)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
let vbox = Box::new(Orientation::Vertical, 5);
|
||||
vbox.append(&search_bar);
|
||||
vbox.append(&scroll_window);
|
||||
|
||||
window.set_child(Some(&vbox));
|
||||
window.present();
|
||||
}
|
48
lact-gui/src/app/root_stack/mod.rs
Normal file
48
lact-gui/src/app/root_stack/mod.rs
Normal file
@ -0,0 +1,48 @@
|
||||
mod info_page;
|
||||
mod oc_page;
|
||||
mod software_page;
|
||||
mod thermals_page;
|
||||
|
||||
use gtk::*;
|
||||
|
||||
use self::software_page::software_page;
|
||||
use info_page::InformationPage;
|
||||
use lact_client::schema::SystemInfo;
|
||||
use oc_page::OcPage;
|
||||
use thermals_page::ThermalsPage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RootStack {
|
||||
pub container: Stack,
|
||||
pub info_page: InformationPage,
|
||||
pub thermals_page: ThermalsPage,
|
||||
pub oc_page: OcPage,
|
||||
}
|
||||
|
||||
impl RootStack {
|
||||
pub fn new(system_info: SystemInfo, embedded_daemon: bool) -> Self {
|
||||
let container = Stack::builder().vexpand(true).build();
|
||||
|
||||
let info_page = InformationPage::new();
|
||||
|
||||
container.add_titled(&info_page.container, Some("info_page"), "Information");
|
||||
|
||||
let oc_page = OcPage::new(&system_info);
|
||||
|
||||
container.add_titled(&oc_page.container, Some("oc_page"), "OC");
|
||||
|
||||
let thermals_page = ThermalsPage::new();
|
||||
|
||||
container.add_titled(&thermals_page.container, Some("thermals_page"), "Thermals");
|
||||
|
||||
let software_page = software_page(system_info, embedded_daemon);
|
||||
container.add_titled(&software_page, Some("software_page"), "Software");
|
||||
|
||||
Self {
|
||||
container,
|
||||
info_page,
|
||||
thermals_page,
|
||||
oc_page,
|
||||
}
|
||||
}
|
||||
}
|
180
lact-gui/src/app/root_stack/oc_page/clocks_frame.rs
Normal file
180
lact-gui/src/app/root_stack/oc_page/clocks_frame.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use super::section_box;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{ClocksTable, ClocksTableGen};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClocksFrame {
|
||||
pub container: Box,
|
||||
tweaking_grid: Grid,
|
||||
max_sclk_adjustment: Adjustment,
|
||||
max_mclk_adjustment: Adjustment,
|
||||
max_voltage_adjustment: Adjustment,
|
||||
reset_button: Button,
|
||||
clocks_data_unavailable_label: Label,
|
||||
}
|
||||
|
||||
impl ClocksFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = section_box("Maximum Clocks", 0, 5);
|
||||
|
||||
let tweaking_grid = Grid::new();
|
||||
let max_sclk_adjustment = oc_adjustment("GPU Clock (MHz)", &tweaking_grid, 0);
|
||||
let max_voltage_adjustment = oc_adjustment("GPU voltage (mV)", &tweaking_grid, 1);
|
||||
let max_mclk_adjustment = oc_adjustment("VRAM Clock (MHz)", &tweaking_grid, 2);
|
||||
|
||||
let reset_button = Button::builder()
|
||||
.label("Defaults")
|
||||
.halign(Align::End)
|
||||
.build();
|
||||
tweaking_grid.attach(&reset_button, 4, 3, 1, 1);
|
||||
|
||||
let clocks_data_unavailable_label = Label::new(Some("No clocks data available"));
|
||||
|
||||
container.append(&tweaking_grid);
|
||||
container.append(&clocks_data_unavailable_label);
|
||||
|
||||
Self {
|
||||
container,
|
||||
tweaking_grid,
|
||||
max_sclk_adjustment,
|
||||
max_mclk_adjustment,
|
||||
max_voltage_adjustment,
|
||||
reset_button,
|
||||
clocks_data_unavailable_label,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_table(&self, table: ClocksTableGen) -> anyhow::Result<()> {
|
||||
if let Some((current_sclk_max, sclk_min, sclk_max)) =
|
||||
extract_value_and_range(&table, |table| {
|
||||
(table.get_max_sclk(), table.get_max_sclk_range())
|
||||
})
|
||||
{
|
||||
self.max_sclk_adjustment.set_lower(sclk_min.into());
|
||||
self.max_sclk_adjustment.set_upper(sclk_max.into());
|
||||
self.max_sclk_adjustment.set_value(current_sclk_max.into());
|
||||
}
|
||||
|
||||
if let Some((current_mclk_max, mclk_min, mclk_max)) =
|
||||
extract_value_and_range(&table, |table| {
|
||||
(table.get_max_mclk(), table.get_max_mclk_range())
|
||||
})
|
||||
{
|
||||
self.max_mclk_adjustment.set_lower(mclk_min.into());
|
||||
self.max_mclk_adjustment.set_upper(mclk_max.into());
|
||||
self.max_mclk_adjustment.set_value(current_mclk_max.into());
|
||||
}
|
||||
|
||||
if let Some((current_voltage_max, voltage_min, voltage_max)) =
|
||||
extract_value_and_range(&table, |table| {
|
||||
(table.get_max_sclk_voltage(), table.get_max_voltage_range())
|
||||
})
|
||||
{
|
||||
self.max_voltage_adjustment.set_lower(voltage_min.into());
|
||||
self.max_voltage_adjustment.set_upper(voltage_max.into());
|
||||
self.max_voltage_adjustment
|
||||
.set_value(current_voltage_max.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.tweaking_grid.show();
|
||||
self.clocks_data_unavailable_label.hide();
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.tweaking_grid.hide();
|
||||
self.clocks_data_unavailable_label.show();
|
||||
}
|
||||
|
||||
pub fn connect_clocks_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
let f = clone!(@strong f => move |_: &Adjustment| f());
|
||||
self.max_sclk_adjustment.connect_value_changed(f.clone());
|
||||
self.max_mclk_adjustment.connect_value_changed(f.clone());
|
||||
self.max_voltage_adjustment.connect_value_changed(f);
|
||||
}
|
||||
|
||||
pub fn connect_clocks_reset<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.reset_button.connect_clicked(move |_| f());
|
||||
}
|
||||
|
||||
pub fn get_settings(&self) -> ClocksSettings {
|
||||
if self.tweaking_grid.is_visible() {
|
||||
let max_core_clock = zero_to_option(self.max_sclk_adjustment.value());
|
||||
let max_memory_clock = zero_to_option(self.max_mclk_adjustment.value());
|
||||
let max_voltage = zero_to_option(self.max_voltage_adjustment.value());
|
||||
|
||||
ClocksSettings {
|
||||
max_core_clock,
|
||||
max_memory_clock,
|
||||
max_voltage,
|
||||
}
|
||||
} else {
|
||||
ClocksSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_value_and_range(
|
||||
table: &ClocksTableGen,
|
||||
f: fn(&ClocksTableGen) -> (Option<u32>, Option<lact_client::schema::Range>),
|
||||
) -> Option<(u32, u32, u32)> {
|
||||
let (maybe_value, maybe_range) = f(table);
|
||||
let (value, range) = maybe_value.zip(maybe_range)?;
|
||||
let (min, max) = range.try_into().ok()?;
|
||||
Some((value, min, max))
|
||||
}
|
||||
|
||||
fn oc_adjustment(title: &'static str, grid: &Grid, row: i32) -> Adjustment {
|
||||
let label = Label::new(Some(title));
|
||||
|
||||
let adjustment = Adjustment::new(0.0, 0.0, 0.0, 1.0, 10.0, 0.0);
|
||||
|
||||
let scale = Scale::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.adjustment(&adjustment)
|
||||
.hexpand(true)
|
||||
.round_digits(0)
|
||||
.digits(0)
|
||||
.value_pos(PositionType::Right)
|
||||
.build();
|
||||
|
||||
let value_selector = SpinButton::new(Some(&adjustment), 1.0, 0);
|
||||
let value_label = Label::new(None);
|
||||
|
||||
adjustment.connect_value_changed(clone!(@strong value_label => move |adjustment| {
|
||||
let value = adjustment.value();
|
||||
value_label.set_text(&value.to_string());
|
||||
}));
|
||||
|
||||
let popover = Popover::builder().child(&value_selector).build();
|
||||
let value_button = MenuButton::builder()
|
||||
.popover(&popover)
|
||||
.child(&value_label)
|
||||
.build();
|
||||
|
||||
grid.attach(&label, 0, row, 1, 1);
|
||||
grid.attach(&scale, 1, row, 4, 1);
|
||||
grid.attach(&value_button, 6, row, 4, 1);
|
||||
|
||||
adjustment
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ClocksSettings {
|
||||
pub max_core_clock: Option<u32>,
|
||||
pub max_memory_clock: Option<u32>,
|
||||
pub max_voltage: Option<u32>,
|
||||
}
|
||||
|
||||
fn zero_to_option(value: f64) -> Option<u32> {
|
||||
if value == 0.0 {
|
||||
None
|
||||
} else {
|
||||
Some(value as u32)
|
||||
}
|
||||
}
|
152
lact-gui/src/app/root_stack/oc_page/mod.rs
Normal file
152
lact-gui/src/app/root_stack/oc_page/mod.rs
Normal file
@ -0,0 +1,152 @@
|
||||
mod clocks_frame;
|
||||
mod performance_level_frame;
|
||||
mod power_cap_frame;
|
||||
mod stats_grid;
|
||||
|
||||
use clocks_frame::ClocksFrame;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{ClocksTableGen, DeviceStats, PerformanceLevel, SystemInfo};
|
||||
use performance_level_frame::PerformanceLevelFrame;
|
||||
use power_cap_frame::PowerCapFrame;
|
||||
use stats_grid::StatsGrid;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OcPage {
|
||||
pub container: Box,
|
||||
stats_grid: StatsGrid,
|
||||
performance_level_frame: PerformanceLevelFrame,
|
||||
power_cap_frame: PowerCapFrame,
|
||||
pub clocks_frame: ClocksFrame,
|
||||
}
|
||||
|
||||
impl OcPage {
|
||||
pub fn new(system_info: &SystemInfo) -> Self {
|
||||
let container = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(5)
|
||||
.margin_top(5)
|
||||
.margin_bottom(5)
|
||||
.build();
|
||||
|
||||
if system_info.amdgpu_overdrive_enabled == Some(false) {
|
||||
let warning_frame = oc_warning_frame();
|
||||
container.append(&warning_frame);
|
||||
}
|
||||
|
||||
let stats_grid = StatsGrid::new();
|
||||
|
||||
container.append(&stats_grid.container);
|
||||
|
||||
let power_cap_frame = PowerCapFrame::new();
|
||||
let performance_level_frame = PerformanceLevelFrame::new();
|
||||
let clocks_frame = ClocksFrame::new();
|
||||
|
||||
container.append(&power_cap_frame.container);
|
||||
container.append(&performance_level_frame.container);
|
||||
container.append(&clocks_frame.container);
|
||||
|
||||
Self {
|
||||
container,
|
||||
stats_grid,
|
||||
performance_level_frame,
|
||||
clocks_frame,
|
||||
power_cap_frame,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &DeviceStats, initial: bool) {
|
||||
self.stats_grid.set_stats(stats);
|
||||
if initial {
|
||||
self.power_cap_frame.set_data(
|
||||
stats.power.cap_current,
|
||||
stats.power.cap_max,
|
||||
stats.power.cap_default,
|
||||
);
|
||||
self.set_performance_level(stats.performance_level);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_clocks_table(&self, table: Option<ClocksTableGen>) {
|
||||
match table {
|
||||
Some(table) => match self.clocks_frame.set_table(table) {
|
||||
Ok(()) => {
|
||||
self.clocks_frame.show();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("got invalid clocks table: {err:?}");
|
||||
self.clocks_frame.hide();
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.clocks_frame.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.performance_level_frame
|
||||
.connect_power_profile_changed(f.clone());
|
||||
self.power_cap_frame.connect_cap_changed(f.clone());
|
||||
self.clocks_frame.connect_clocks_changed(f);
|
||||
}
|
||||
|
||||
pub fn set_performance_level(&self, profile: Option<PerformanceLevel>) {
|
||||
match profile {
|
||||
Some(profile) => {
|
||||
self.performance_level_frame.show();
|
||||
self.performance_level_frame.set_active_profile(profile);
|
||||
}
|
||||
None => self.performance_level_frame.hide(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_performance_level(&self) -> Option<PerformanceLevel> {
|
||||
if self.performance_level_frame.get_visibility() {
|
||||
let level = self
|
||||
.performance_level_frame
|
||||
.get_selected_performance_level();
|
||||
Some(level)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_power_cap(&self) -> Option<f64> {
|
||||
self.power_cap_frame.get_cap()
|
||||
}
|
||||
}
|
||||
|
||||
fn section_box(title: &str, spacing: i32, margin: i32) -> Box {
|
||||
let container = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.spacing(spacing)
|
||||
.margin_start(margin)
|
||||
.margin_end(margin)
|
||||
.build();
|
||||
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.label(format!("<span font_desc='11'><b>{title}</b></span>"))
|
||||
.xalign(0.1)
|
||||
.build();
|
||||
|
||||
container.append(&label);
|
||||
container
|
||||
}
|
||||
|
||||
fn oc_warning_frame() -> Frame {
|
||||
let container = Frame::new(Some("Overclocking information"));
|
||||
|
||||
container.set_label_align(0.3);
|
||||
|
||||
let warning_label = Label::new(None);
|
||||
|
||||
warning_label.set_wrap(true);
|
||||
warning_label.set_markup("Overclocking support is not enabled! To enable overclocking support, you need to add <b>amdgpu.ppfeaturemask=0xffffffff</b> to your kernel boot options. Look for the documentation of your distro.");
|
||||
warning_label.set_selectable(true);
|
||||
|
||||
container.set_child(Some(&warning_label));
|
||||
container
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::PerformanceLevel;
|
||||
|
||||
use super::section_box;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PerformanceLevelFrame {
|
||||
pub container: Box,
|
||||
combo_box: ComboBoxText,
|
||||
}
|
||||
|
||||
impl PerformanceLevelFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = section_box("Performance level", 5, 5);
|
||||
|
||||
let root_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let combo_box = ComboBoxText::new();
|
||||
combo_box.set_sensitive(false);
|
||||
|
||||
combo_box.append(None, "Automatic");
|
||||
combo_box.append(None, "Highest clocks");
|
||||
combo_box.append(None, "Lowest clocks");
|
||||
|
||||
root_box.append(&combo_box);
|
||||
|
||||
let description_label = Label::new(None);
|
||||
|
||||
root_box.append(&description_label);
|
||||
|
||||
{
|
||||
combo_box.connect_changed(move |combobox| match combobox.active().unwrap() {
|
||||
0 => description_label
|
||||
.set_text("Automatically adjust GPU and VRAM clocks. (Default)"),
|
||||
1 => description_label
|
||||
.set_text("Always use the highest clockspeeds for GPU and VRAM."),
|
||||
2 => description_label
|
||||
.set_text("Always use the lowest clockspeeds for GPU and VRAM."),
|
||||
_ => unreachable!(),
|
||||
});
|
||||
}
|
||||
|
||||
container.append(&root_box);
|
||||
Self {
|
||||
container,
|
||||
combo_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_profile(&self, level: PerformanceLevel) {
|
||||
self.combo_box.set_sensitive(true);
|
||||
match level {
|
||||
PerformanceLevel::Auto => self.combo_box.set_active(Some(0)),
|
||||
PerformanceLevel::High => self.combo_box.set_active(Some(1)),
|
||||
PerformanceLevel::Low => self.combo_box.set_active(Some(2)),
|
||||
PerformanceLevel::Manual => todo!(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn connect_power_profile_changed<F: Fn() + 'static>(&self, f: F) {
|
||||
self.combo_box.connect_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_selected_performance_level(&self) -> PerformanceLevel {
|
||||
match self.combo_box.active().unwrap() {
|
||||
0 => PerformanceLevel::Auto,
|
||||
1 => PerformanceLevel::High,
|
||||
2 => PerformanceLevel::Low,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
self.container.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
pub fn get_visibility(&self) -> bool {
|
||||
self.container.get_visible()
|
||||
}
|
||||
}
|
98
lact-gui/src/app/root_stack/oc_page/power_cap_frame.rs
Normal file
98
lact-gui/src/app/root_stack/oc_page/power_cap_frame.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use super::section_box;
|
||||
use gtk::*;
|
||||
use gtk::{glib::clone, prelude::*};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PowerCapFrame {
|
||||
pub container: Box,
|
||||
default_cap: Rc<Cell<Option<f64>>>,
|
||||
adjustment: Adjustment,
|
||||
}
|
||||
|
||||
impl PowerCapFrame {
|
||||
pub fn new() -> Self {
|
||||
let container = section_box("Power Usage Limit", 5, 5);
|
||||
let default_cap = Rc::new(Cell::new(None));
|
||||
|
||||
let value_suffix = "W";
|
||||
let root_box = Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let label = Label::new(None);
|
||||
root_box.append(&label);
|
||||
|
||||
let adjustment = Adjustment::new(0.0, 0.0, 0.0, 1.0, 10.0, 0.0);
|
||||
|
||||
adjustment.connect_value_changed(clone!(@strong label => move |adj| {
|
||||
let text = format!("{}/{} {}", adj.value().round(), adj.upper(), value_suffix);
|
||||
label.set_label(&text);
|
||||
}));
|
||||
|
||||
let scale = Scale::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.adjustment(&adjustment)
|
||||
.hexpand(true)
|
||||
.round_digits(0)
|
||||
.build();
|
||||
|
||||
scale.set_draw_value(false);
|
||||
|
||||
root_box.append(&scale);
|
||||
|
||||
let reset_button = Button::with_label("Default");
|
||||
reset_button.connect_clicked(clone!(@strong adjustment, @strong default_cap => move |_| {
|
||||
if let Some(cap) = default_cap.get() {
|
||||
adjustment.set_value(cap);
|
||||
} else {
|
||||
error!("Could not set default cap, value not provided");
|
||||
}
|
||||
}));
|
||||
root_box.append(&reset_button);
|
||||
|
||||
container.append(&root_box);
|
||||
|
||||
Self {
|
||||
container,
|
||||
adjustment,
|
||||
default_cap,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_data(
|
||||
&self,
|
||||
power_cap: Option<f64>,
|
||||
power_cap_max: Option<f64>,
|
||||
power_cap_default: Option<f64>,
|
||||
) {
|
||||
if let Some(power_cap_max) = power_cap_max {
|
||||
self.adjustment.set_upper(power_cap_max);
|
||||
} else {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
if let Some(power_cap) = power_cap {
|
||||
self.adjustment.set_value(power_cap);
|
||||
} else {
|
||||
self.container.set_visible(false);
|
||||
}
|
||||
|
||||
self.default_cap.set(power_cap_default);
|
||||
}
|
||||
|
||||
pub fn get_cap(&self) -> Option<f64> {
|
||||
// Using match gives a warning that floats shouldn't be used in patterns
|
||||
let cap = self.adjustment.value();
|
||||
if cap == 0.0 {
|
||||
None
|
||||
} else {
|
||||
Some(cap)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_cap_changed<F: Fn() + 'static>(&self, f: F) {
|
||||
self.adjustment.connect_value_changed(move |_| {
|
||||
f();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use daemon::gpu_controller::GpuStats;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{ClockspeedStats, DeviceStats, PowerStats, VoltageStats, VramStats};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StatsGrid {
|
||||
@ -37,7 +37,7 @@ impl StatsGrid {
|
||||
|
||||
vram_usage_label.set_text("0/0 MiB");
|
||||
|
||||
vram_usage_overlay.add(&vram_usage_bar);
|
||||
vram_usage_overlay.set_child(Some(&vram_usage_bar));
|
||||
vram_usage_overlay.add_overlay(&vram_usage_label);
|
||||
|
||||
container.attach(&vram_usage_overlay, 1, 0, 2, 1);
|
||||
@ -47,11 +47,11 @@ impl StatsGrid {
|
||||
{
|
||||
let gpu_clock_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
gpu_clock_box.pack_start(&Label::new(Some("GPU Clock:")), false, false, 2);
|
||||
gpu_clock_box.append(&Label::new(Some("GPU Clock:")));
|
||||
|
||||
gpu_clock_label.set_markup("<b>0MHz</b>");
|
||||
|
||||
gpu_clock_box.pack_start(&gpu_clock_label, false, false, 2);
|
||||
gpu_clock_box.append(&gpu_clock_label);
|
||||
|
||||
gpu_clock_box.set_halign(Align::Center);
|
||||
|
||||
@ -62,11 +62,11 @@ impl StatsGrid {
|
||||
{
|
||||
let vram_clock_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
vram_clock_box.pack_start(&Label::new(Some("VRAM Clock:")), false, false, 2);
|
||||
vram_clock_box.append(&Label::new(Some("VRAM Clock:")));
|
||||
|
||||
vram_clock_label.set_markup("<b>0MHz</b>");
|
||||
|
||||
vram_clock_box.pack_start(&vram_clock_label, false, false, 2);
|
||||
vram_clock_box.append(&vram_clock_label);
|
||||
|
||||
vram_clock_box.set_halign(Align::Center);
|
||||
|
||||
@ -76,11 +76,11 @@ impl StatsGrid {
|
||||
{
|
||||
let gpu_voltage_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
gpu_voltage_box.pack_start(&Label::new(Some("GPU Voltage:")), false, false, 2);
|
||||
gpu_voltage_box.append(&Label::new(Some("GPU Voltage:")));
|
||||
|
||||
gpu_voltage_label.set_markup("<b>0.000V</b>");
|
||||
|
||||
gpu_voltage_box.pack_start(&gpu_voltage_label, false, false, 2);
|
||||
gpu_voltage_box.append(&gpu_voltage_label);
|
||||
|
||||
gpu_voltage_box.set_halign(Align::Center);
|
||||
|
||||
@ -91,11 +91,11 @@ impl StatsGrid {
|
||||
{
|
||||
let power_usage_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
power_usage_box.pack_start(&Label::new(Some("Power Usage:")), false, false, 2);
|
||||
power_usage_box.append(&Label::new(Some("Power Usage:")));
|
||||
|
||||
power_usage_label.set_markup("<b>00/000W</b>");
|
||||
|
||||
power_usage_box.pack_start(&power_usage_label, false, false, 2);
|
||||
power_usage_box.append(&power_usage_label);
|
||||
|
||||
power_usage_box.set_halign(Align::Center);
|
||||
|
||||
@ -106,11 +106,11 @@ impl StatsGrid {
|
||||
{
|
||||
let gpu_temperature_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
gpu_temperature_box.pack_start(&Label::new(Some("GPU Temperature:")), false, false, 2);
|
||||
gpu_temperature_box.append(&Label::new(Some("GPU Temperature:")));
|
||||
|
||||
// gpu_temperature_label.set_markup("<b>0°C</b>");
|
||||
|
||||
gpu_temperature_box.pack_start(&gpu_temperature_label, false, false, 2);
|
||||
gpu_temperature_box.append(&gpu_temperature_label);
|
||||
|
||||
gpu_temperature_box.set_halign(Align::Center);
|
||||
|
||||
@ -121,9 +121,9 @@ impl StatsGrid {
|
||||
{
|
||||
let gpu_usage_box = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
gpu_usage_box.pack_start(&Label::new(Some("GPU Usage:")), false, false, 2);
|
||||
gpu_usage_box.append(&Label::new(Some("GPU Usage:")));
|
||||
|
||||
gpu_usage_box.pack_start(&gpu_usage_label, false, false, 2);
|
||||
gpu_usage_box.append(&gpu_usage_label);
|
||||
|
||||
gpu_usage_box.set_halign(Align::Center);
|
||||
|
||||
@ -143,52 +143,66 @@ impl StatsGrid {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &GpuStats) {
|
||||
self.vram_usage_bar.set_value(
|
||||
stats.mem_used.unwrap_or_else(|| 0) as f64
|
||||
/ stats.mem_total.unwrap_or_else(|| 0) as f64,
|
||||
);
|
||||
pub fn set_stats(&self, stats: &DeviceStats) {
|
||||
let VramStats {
|
||||
total: total_vram,
|
||||
used: used_vram,
|
||||
} = stats.vram;
|
||||
|
||||
if let (Some(used_vram), Some(total_vram)) = (used_vram, total_vram) {
|
||||
self.vram_usage_bar
|
||||
.set_value(used_vram as f64 / total_vram as f64);
|
||||
}
|
||||
self.vram_usage_label.set_text(&format!(
|
||||
"{}/{} MiB",
|
||||
stats.mem_used.unwrap_or_else(|| 0),
|
||||
stats.mem_total.unwrap_or_else(|| 0)
|
||||
used_vram.unwrap_or(0) / 1024 / 1024,
|
||||
total_vram.unwrap_or(0) / 1024 / 1024,
|
||||
));
|
||||
|
||||
self.gpu_clock_label.set_markup(&format!(
|
||||
"<b>{}MHz</b>",
|
||||
stats.gpu_freq.unwrap_or_else(|| 0)
|
||||
));
|
||||
let ClockspeedStats {
|
||||
gpu_clockspeed,
|
||||
vram_clockspeed,
|
||||
} = stats.clockspeed;
|
||||
|
||||
self.vram_clock_label.set_markup(&format!(
|
||||
"<b>{}MHz</b>",
|
||||
stats.mem_freq.unwrap_or_else(|| 0)
|
||||
));
|
||||
self.gpu_clock_label
|
||||
.set_markup(&format!("<b>{}MHz</b>", gpu_clockspeed.unwrap_or(0)));
|
||||
self.vram_clock_label
|
||||
.set_markup(&format!("<b>{}MHz</b>", vram_clockspeed.unwrap_or(0)));
|
||||
|
||||
let VoltageStats {
|
||||
gpu: gpu_voltage, ..
|
||||
} = stats.voltage;
|
||||
|
||||
self.gpu_voltage_label.set_markup(&format!(
|
||||
"<b>{}V</b>",
|
||||
stats.voltage.unwrap_or_else(|| 0) as f64 / 1000f64
|
||||
gpu_voltage.unwrap_or(0) as f64 / 1000f64
|
||||
));
|
||||
|
||||
let PowerStats {
|
||||
average: power_average,
|
||||
cap_current: power_cap_current,
|
||||
..
|
||||
} = stats.power;
|
||||
|
||||
self.power_usage_label.set_markup(&format!(
|
||||
"<b>{}/{}W</b>",
|
||||
stats.power_avg.unwrap_or_else(|| 0),
|
||||
stats.power_cap.unwrap_or_else(|| 0)
|
||||
power_average.unwrap_or(0.0),
|
||||
power_cap_current.unwrap_or(0.0)
|
||||
));
|
||||
|
||||
let temp = match stats.temperatures.get("junction") {
|
||||
Some(temp) => Some(temp.current),
|
||||
None => match stats.temperatures.get("edge") {
|
||||
Some(temp) => Some(temp.current),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
let maybe_temp = stats
|
||||
.temps
|
||||
.get("junction")
|
||||
.or_else(|| stats.temps.get("edge"));
|
||||
|
||||
if let Some(temp) = temp {
|
||||
if let Some(temp) = maybe_temp.and_then(|temp| temp.current) {
|
||||
self.gpu_temperature_label
|
||||
.set_markup(&format!("<b>{}°C</b>", temp));
|
||||
.set_markup(&format!("<b>{temp}°C</b>"));
|
||||
}
|
||||
|
||||
self.gpu_usage_label
|
||||
.set_markup(&format!("<b>{}%</b>", stats.gpu_usage.unwrap_or_default()));
|
||||
self.gpu_usage_label.set_markup(&format!(
|
||||
"<b>{}%</b>",
|
||||
stats.busy_percent.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
}
|
61
lact-gui/src/app/root_stack/software_page.rs
Normal file
61
lact-gui/src/app/root_stack/software_page.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::SystemInfo;
|
||||
|
||||
pub fn software_page(system_info: SystemInfo, embedded: bool) -> Grid {
|
||||
let container = Grid::new();
|
||||
|
||||
container.set_margin_start(5);
|
||||
container.set_margin_end(5);
|
||||
container.set_margin_bottom(5);
|
||||
container.set_margin_top(5);
|
||||
|
||||
container.set_column_spacing(5);
|
||||
|
||||
container.attach(
|
||||
&{
|
||||
let label = Label::new(None);
|
||||
label.set_markup("LACT Daemon:");
|
||||
label.set_halign(Align::End);
|
||||
label.set_hexpand(true);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
|
||||
if embedded {
|
||||
daemon_version.push_str("-embedded");
|
||||
}
|
||||
let version_label = Label::builder()
|
||||
.use_markup(true)
|
||||
.label(format!("<b>{daemon_version}</b>"))
|
||||
.hexpand(true)
|
||||
.halign(Align::Start)
|
||||
.build();
|
||||
|
||||
container.attach(&version_label, 1, 0, 1, 1);
|
||||
|
||||
container.attach(
|
||||
&Label::builder()
|
||||
.label("Kernel version:")
|
||||
.halign(Align::End)
|
||||
.hexpand(true)
|
||||
.build(),
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
let kernel_version_label = Label::builder()
|
||||
.use_markup(true)
|
||||
.label(format!("<b>{}</b>", system_info.kernel_version))
|
||||
.hexpand(true)
|
||||
.halign(Align::Start)
|
||||
.build();
|
||||
container.attach(&kernel_version_label, 1, 1, 1, 1);
|
||||
|
||||
container
|
||||
}
|
175
lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs
Normal file
175
lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs
Normal file
@ -0,0 +1,175 @@
|
||||
mod point_adjustment;
|
||||
|
||||
use self::point_adjustment::PointAdjustment;
|
||||
use glib::clone;
|
||||
use gtk::graphene::Point;
|
||||
use gtk::gsk::Transform;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{default_fan_curve, FanCurveMap};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FanCurveFrame {
|
||||
pub container: Box,
|
||||
curve_container: Frame,
|
||||
points: Rc<RefCell<Vec<PointAdjustment>>>,
|
||||
}
|
||||
|
||||
impl FanCurveFrame {
|
||||
pub fn new() -> Self {
|
||||
let root_box = Box::new(Orientation::Vertical, 5);
|
||||
root_box.hide();
|
||||
|
||||
let hbox = Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let curve_container = Frame::new(Some("Fan Curve"));
|
||||
|
||||
curve_container.set_margin_start(10);
|
||||
curve_container.set_margin_end(10);
|
||||
curve_container.set_margin_top(10);
|
||||
|
||||
let ratio_title_label = Label::builder().label("Fan speed (%)").build();
|
||||
|
||||
let fixed = Fixed::new();
|
||||
fixed.put(&ratio_title_label, 0.0, 0.0);
|
||||
|
||||
// This is a workaround to rotate the label that only looks good at the default window size
|
||||
// Unfortunately there's no other way to do this (short of implementing custom rendering for a widget) as gtk4 removed the `angle` property for labels
|
||||
let rotation_transform = Transform::new()
|
||||
.rotate(-90.0)
|
||||
.translate(&Point::new(-200.0, 10.0));
|
||||
fixed.set_child_transform(&ratio_title_label, Some(&rotation_transform));
|
||||
|
||||
hbox.append(&fixed);
|
||||
hbox.append(&curve_container);
|
||||
|
||||
let temperature_title_label = Label::new(Some("Temperature (°C)"));
|
||||
|
||||
let buttons_box = Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(5)
|
||||
.halign(Align::End)
|
||||
.build();
|
||||
|
||||
let add_button = Button::builder().icon_name("list-add-symbolic").build();
|
||||
let remove_button = Button::builder().icon_name("list-remove-symbolic").build();
|
||||
let default_button = Button::builder().label("Default").build();
|
||||
|
||||
buttons_box.append(&default_button);
|
||||
buttons_box.append(&remove_button);
|
||||
buttons_box.append(&add_button);
|
||||
|
||||
root_box.append(&hbox);
|
||||
root_box.append(&temperature_title_label);
|
||||
root_box.append(&buttons_box);
|
||||
|
||||
let points = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let curve_frame = Self {
|
||||
container: root_box,
|
||||
curve_container,
|
||||
points,
|
||||
};
|
||||
|
||||
default_button.connect_clicked(clone!(@strong curve_frame => move |_| {
|
||||
let curve = default_fan_curve();
|
||||
curve_frame.set_curve(&curve);
|
||||
}));
|
||||
|
||||
add_button.connect_clicked(clone!(@strong curve_frame => move |_| {
|
||||
curve_frame.add_point();
|
||||
}));
|
||||
|
||||
remove_button.connect_clicked(clone!(@strong curve_frame => move |_| {
|
||||
curve_frame.remove_point();
|
||||
}));
|
||||
|
||||
curve_frame
|
||||
}
|
||||
|
||||
fn add_point(&self) {
|
||||
let mut curve = self.get_curve();
|
||||
if let Some((temperature, ratio)) = curve.iter().last() {
|
||||
curve.insert(temperature + 5, *ratio);
|
||||
self.set_curve(&curve);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_point(&self) {
|
||||
let mut curve = self.get_curve();
|
||||
curve.pop_last();
|
||||
self.set_curve(&curve);
|
||||
}
|
||||
|
||||
fn notify_changed(&self) {
|
||||
if let Some(point) = self.points.borrow().first() {
|
||||
point.ratio.emit_by_name::<()>("value-changed", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_curve(&self, curve: &FanCurveMap) {
|
||||
// Notify that the values were changed when the entire curve is overwritten, e.g. when resetting to default
|
||||
self.notify_changed();
|
||||
|
||||
let points_container = Box::builder()
|
||||
.orientation(Orientation::Horizontal)
|
||||
.spacing(5)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
|
||||
let mut adjustments = Vec::with_capacity(curve.len());
|
||||
|
||||
for (temperature, ratio) in curve {
|
||||
let adjustment = PointAdjustment::new(&points_container, *ratio, *temperature);
|
||||
adjustments.push(adjustment);
|
||||
}
|
||||
|
||||
self.points.replace(adjustments);
|
||||
self.curve_container.set_child(Some(&points_container));
|
||||
}
|
||||
|
||||
pub fn get_curve(&self) -> FanCurveMap {
|
||||
let mut curve = BTreeMap::new();
|
||||
|
||||
let points = self.points.borrow();
|
||||
for point in &*points {
|
||||
let temperature = point.temperature.value() as i32;
|
||||
let ratio = point.ratio.value() as f32;
|
||||
curve.insert(temperature, ratio);
|
||||
}
|
||||
|
||||
curve
|
||||
}
|
||||
|
||||
pub fn connect_adjusted<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
let closure = clone!(@strong f => move |_: &Adjustment| {
|
||||
f();
|
||||
});
|
||||
|
||||
for point in &*self.points.borrow() {
|
||||
point.ratio.connect_value_changed(closure.clone());
|
||||
point.temperature.connect_value_changed(closure.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "gtk-tests"))]
|
||||
mod tests {
|
||||
use super::FanCurveFrame;
|
||||
use lact_client::schema::default_fan_curve;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn set_get_curve() {
|
||||
gtk::init().unwrap();
|
||||
|
||||
let curve = default_fan_curve();
|
||||
let frame = FanCurveFrame::new();
|
||||
frame.set_curve(&curve);
|
||||
let received_curve = frame.get_curve();
|
||||
assert_eq!(received_curve, curve);
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
use glib::clone;
|
||||
use gtk::{
|
||||
glib,
|
||||
traits::{AdjustmentExt, BoxExt},
|
||||
Adjustment, Box, Label, MenuButton, Orientation, Popover, Scale, SpinButton,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PointAdjustment {
|
||||
pub temperature: Adjustment,
|
||||
pub ratio: Adjustment,
|
||||
}
|
||||
|
||||
impl PointAdjustment {
|
||||
pub fn new(parent: &Box, ratio: f32, temperature: i32) -> Self {
|
||||
let container = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
let ratio_adjustment = Adjustment::new(ratio.into(), 0.0, 1.0, 0.01, 0.05, 0.00);
|
||||
let scale = Scale::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.adjustment(&ratio_adjustment)
|
||||
.hexpand(true)
|
||||
.vexpand(true)
|
||||
.inverted(true)
|
||||
.build();
|
||||
container.append(&scale);
|
||||
|
||||
let temperature_adjustment = Adjustment::new(temperature.into(), 0.0, 100.0, 1.0, 1.0, 0.0);
|
||||
let temperature_selector = SpinButton::new(Some(&temperature_adjustment), 1.0, 0);
|
||||
|
||||
// Using the built-in MenuButton label function creates an empty icon
|
||||
let temperature_label = Label::new(Some(&temperature.to_string()));
|
||||
|
||||
temperature_adjustment.connect_value_changed(
|
||||
clone!(@strong temperature_label => move |temperature_adjustment| {
|
||||
let temperature = temperature_adjustment.value();
|
||||
temperature_label.set_text(&temperature.to_string());
|
||||
}),
|
||||
);
|
||||
|
||||
let popover = Popover::builder().child(&temperature_selector).build();
|
||||
let temperature_button = MenuButton::builder()
|
||||
.popover(&popover)
|
||||
.child(&temperature_label)
|
||||
.build();
|
||||
|
||||
container.append(&temperature_button);
|
||||
|
||||
parent.append(&container);
|
||||
|
||||
Self {
|
||||
temperature: temperature_adjustment,
|
||||
ratio: ratio_adjustment,
|
||||
}
|
||||
}
|
||||
}
|
207
lact-gui/src/app/root_stack/thermals_page/mod.rs
Normal file
207
lact-gui/src/app/root_stack/thermals_page/mod.rs
Normal file
@ -0,0 +1,207 @@
|
||||
mod fan_curve_frame;
|
||||
|
||||
use fan_curve_frame::FanCurveFrame;
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::{default_fan_curve, DeviceStats, FanCurveMap};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThermalsSettings {
|
||||
pub manual_fan_control: bool,
|
||||
pub curve: Option<FanCurveMap>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThermalsPage {
|
||||
pub container: Box,
|
||||
temp_label: Label,
|
||||
fan_speed_label: Label,
|
||||
fan_control_enabled_switch: Switch,
|
||||
fan_curve_frame: FanCurveFrame,
|
||||
}
|
||||
|
||||
impl ThermalsPage {
|
||||
pub fn new() -> Self {
|
||||
let container = Box::new(Orientation::Vertical, 5);
|
||||
|
||||
let grid = Grid::new();
|
||||
|
||||
grid.set_margin_start(5);
|
||||
grid.set_margin_end(5);
|
||||
grid.set_margin_bottom(5);
|
||||
grid.set_margin_top(5);
|
||||
|
||||
grid.set_column_homogeneous(true);
|
||||
|
||||
grid.set_row_spacing(7);
|
||||
grid.set_column_spacing(5);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Temperatures:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let temp_label = Label::new(None);
|
||||
temp_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&temp_label, 2, 0, 1, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Fan speed:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let fan_speed_label = Label::new(None);
|
||||
fan_speed_label.set_halign(Align::Start);
|
||||
|
||||
grid.attach(&fan_speed_label, 2, 1, 1, 1);
|
||||
|
||||
grid.attach(
|
||||
&{
|
||||
let label = Label::new(Some("Automatic fan control:"));
|
||||
label.set_halign(Align::End);
|
||||
label
|
||||
},
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
);
|
||||
|
||||
let fan_control_enabled_switch = Switch::builder()
|
||||
.active(true)
|
||||
.halign(Align::Start)
|
||||
.sensitive(false)
|
||||
.build();
|
||||
|
||||
grid.attach(&fan_control_enabled_switch, 2, 2, 1, 1);
|
||||
|
||||
container.prepend(&grid);
|
||||
|
||||
let fan_curve_frame = FanCurveFrame::new();
|
||||
|
||||
container.append(&fan_curve_frame.container);
|
||||
|
||||
// Show/hide fan curve when the switch is toggled
|
||||
{
|
||||
let fan_curve_frame = fan_curve_frame.clone();
|
||||
fan_control_enabled_switch.connect_state_set(move |_, state| {
|
||||
if state {
|
||||
show_fan_control_warning();
|
||||
fan_curve_frame.container.hide();
|
||||
} else {
|
||||
fan_curve_frame.container.show();
|
||||
}
|
||||
Inhibit(false)
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
container,
|
||||
temp_label,
|
||||
fan_speed_label,
|
||||
fan_control_enabled_switch,
|
||||
fan_curve_frame,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_stats(&self, stats: &DeviceStats, initial: bool) {
|
||||
let mut temperatures: Vec<String> = stats
|
||||
.temps
|
||||
.iter()
|
||||
.filter_map(|(label, temp)| temp.current.map(|current| format!("{label}: {current}°C")))
|
||||
.collect();
|
||||
temperatures.sort();
|
||||
let temperatures_text = if temperatures.is_empty() {
|
||||
String::from("No sensors found")
|
||||
} else {
|
||||
temperatures.join("\n")
|
||||
};
|
||||
|
||||
self.temp_label
|
||||
.set_markup(&format!("<b>{temperatures_text}</b>",));
|
||||
|
||||
match stats.fan.speed_current {
|
||||
Some(fan_speed_current) => self.fan_speed_label.set_markup(&format!(
|
||||
"<b>{} RPM ({}%)</b>",
|
||||
fan_speed_current,
|
||||
(fan_speed_current as f64
|
||||
/ stats.fan.speed_max.unwrap_or(fan_speed_current) as f64
|
||||
* 100.0)
|
||||
.round()
|
||||
)),
|
||||
None => self.fan_speed_label.set_text("No fan detected"),
|
||||
}
|
||||
|
||||
if initial {
|
||||
self.fan_control_enabled_switch.set_visible(true);
|
||||
self.fan_control_enabled_switch
|
||||
.set_sensitive(stats.fan.speed_current.is_some());
|
||||
self.fan_control_enabled_switch
|
||||
.set_active(!stats.fan.control_enabled);
|
||||
|
||||
if let Some(curve) = &stats.fan.curve {
|
||||
self.fan_curve_frame.set_curve(curve);
|
||||
}
|
||||
|
||||
if stats.fan.control_enabled {
|
||||
self.fan_curve_frame.container.show();
|
||||
} else {
|
||||
self.fan_curve_frame.container.hide();
|
||||
if self.fan_curve_frame.get_curve().is_empty() {
|
||||
self.fan_curve_frame.set_curve(&default_fan_curve());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_settings_changed<F: Fn() + 'static + Clone>(&self, f: F) {
|
||||
self.fan_control_enabled_switch
|
||||
.connect_state_set(clone!(@strong f => move |_, _| {
|
||||
f();
|
||||
Inhibit(false)
|
||||
}));
|
||||
|
||||
self.fan_curve_frame.connect_adjusted(move || {
|
||||
f();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_thermals_settings(&self) -> Option<ThermalsSettings> {
|
||||
if self.fan_control_enabled_switch.is_sensitive() {
|
||||
let manual_fan_control = !self.fan_control_enabled_switch.state();
|
||||
let curve = self.fan_curve_frame.get_curve();
|
||||
let curve = if curve.is_empty() { None } else { Some(curve) };
|
||||
|
||||
Some(ThermalsSettings {
|
||||
manual_fan_control,
|
||||
curve,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_fan_control_warning() {
|
||||
let diag = MessageDialog::new(None::<&Window>, DialogFlags::empty(), MessageType::Warning, ButtonsType::Ok,
|
||||
"Warning! Due to a driver bug, a reboot may be required for fan control to properly switch back to automatic.");
|
||||
diag.run_async(|diag, _| {
|
||||
diag.hide();
|
||||
})
|
||||
}
|
46
lact-gui/src/lib.rs
Normal file
46
lact-gui/src/lib.rs
Normal file
@ -0,0 +1,46 @@
|
||||
mod app;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use app::App;
|
||||
use lact_client::DaemonClient;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use tracing::{error, info, metadata::LevelFilter};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
const APP_ID: &str = "io.github.lact-linux";
|
||||
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
if let Err(err) = gtk::init() {
|
||||
return Err(anyhow!("Cannot initialize GTK: {err}"));
|
||||
}
|
||||
|
||||
let connection = create_connection()?;
|
||||
let app = App::new(connection);
|
||||
|
||||
app.run()
|
||||
}
|
||||
|
||||
fn create_connection() -> anyhow::Result<DaemonClient> {
|
||||
match DaemonClient::connect() {
|
||||
Ok(connection) => Ok(connection),
|
||||
Err(err) => {
|
||||
info!("could not connect to socket: {err}");
|
||||
info!("using a local daemon");
|
||||
|
||||
let (server_stream, client_stream) = UnixStream::pair()?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
if let Err(err) = lact_daemon::run_embedded(server_stream) {
|
||||
error!("Builtin daemon error: {err}");
|
||||
}
|
||||
});
|
||||
|
||||
DaemonClient::from_stream(client_stream, true)
|
||||
}
|
||||
}
|
||||
}
|
12
lact-schema/Cargo.toml
Normal file
12
lact-schema/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "lact-schema"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
amdgpu-sysfs = { version = "0.9.3", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
indexmap = { version = "*", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
175
lact-schema/src/lib.rs
Normal file
175
lact-schema/src/lib.rs
Normal file
@ -0,0 +1,175 @@
|
||||
pub mod request;
|
||||
mod response;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use amdgpu_sysfs::{
|
||||
gpu_handle::{
|
||||
overdrive::{ClocksTable, ClocksTableGen, Range},
|
||||
PerformanceLevel, PowerLevels,
|
||||
},
|
||||
hw_mon::Temperature,
|
||||
};
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
pub type FanCurveMap = BTreeMap<i32, f32>;
|
||||
|
||||
pub fn default_fan_curve() -> FanCurveMap {
|
||||
[
|
||||
(30, 0.0),
|
||||
(40, 0.2),
|
||||
(50, 0.35),
|
||||
(60, 0.5),
|
||||
(70, 0.75),
|
||||
(80, 1.0),
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Pong;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SystemInfo<'a> {
|
||||
pub version: &'a str,
|
||||
pub profile: &'a str,
|
||||
pub kernel_version: String,
|
||||
pub amdgpu_overdrive_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceListEntry<'a> {
|
||||
pub id: &'a str,
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GpuPciInfo {
|
||||
pub device_pci_info: PciInfo,
|
||||
pub subsystem_pci_info: PciInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceInfo<'a> {
|
||||
#[serde(borrow)]
|
||||
pub pci_info: Option<Cow<'a, GpuPciInfo>>,
|
||||
pub vulkan_info: Option<VulkanInfo>,
|
||||
pub driver: &'a str,
|
||||
pub vbios_version: Option<String>,
|
||||
pub link_info: LinkInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct ClocksInfo {
|
||||
pub max_sclk: Option<u32>,
|
||||
pub max_mclk: Option<u32>,
|
||||
pub max_voltage: Option<u32>,
|
||||
pub table: Option<ClocksTableGen>,
|
||||
}
|
||||
|
||||
impl From<ClocksTableGen> for ClocksInfo {
|
||||
fn from(table: ClocksTableGen) -> Self {
|
||||
let max_sclk = table.get_max_sclk();
|
||||
let max_mclk = table.get_max_mclk();
|
||||
let max_voltage = table.get_max_sclk_voltage();
|
||||
Self {
|
||||
max_sclk,
|
||||
max_mclk,
|
||||
max_voltage,
|
||||
table: Some(table),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LinkInfo {
|
||||
pub current_width: Option<String>,
|
||||
pub current_speed: Option<String>,
|
||||
pub max_width: Option<String>,
|
||||
pub max_speed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VulkanInfo {
|
||||
pub device_name: String,
|
||||
pub api_version: String,
|
||||
pub driver: VulkanDriverInfo,
|
||||
pub enabled_layers: Vec<String>,
|
||||
pub features: IndexMap<Cow<'static, str>, bool>,
|
||||
pub extensions: IndexMap<Cow<'static, str>, bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct VulkanDriverInfo {
|
||||
pub version: u32,
|
||||
pub name: Option<String>,
|
||||
pub info: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PciInfo {
|
||||
pub vendor_id: String,
|
||||
pub vendor: Option<String>,
|
||||
pub model_id: String,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct DeviceStats {
|
||||
pub fan: FanStats,
|
||||
pub clockspeed: ClockspeedStats,
|
||||
pub voltage: VoltageStats,
|
||||
pub vram: VramStats,
|
||||
pub power: PowerStats,
|
||||
pub temps: HashMap<String, Temperature>,
|
||||
pub busy_percent: Option<u8>,
|
||||
pub performance_level: Option<PerformanceLevel>,
|
||||
pub core_clock_levels: Option<PowerLevels<u64>>,
|
||||
pub memory_clock_levels: Option<PowerLevels<u64>>,
|
||||
pub pcie_clock_levels: Option<PowerLevels<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FanStats {
|
||||
pub control_enabled: bool,
|
||||
pub curve: Option<FanCurveMap>,
|
||||
pub speed_current: Option<u32>,
|
||||
pub speed_max: Option<u32>,
|
||||
pub speed_min: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct ClockspeedStats {
|
||||
pub gpu_clockspeed: Option<u64>,
|
||||
pub vram_clockspeed: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct VoltageStats {
|
||||
pub gpu: Option<u64>,
|
||||
pub northbridge: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct VramStats {
|
||||
pub total: Option<u64>,
|
||||
pub used: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||
pub struct PowerStats {
|
||||
pub average: Option<f64>,
|
||||
pub cap_current: Option<f64>,
|
||||
pub cap_max: Option<f64>,
|
||||
pub cap_min: Option<f64>,
|
||||
pub cap_default: Option<f64>,
|
||||
}
|
46
lact-schema/src/request.rs
Normal file
46
lact-schema/src/request.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::FanCurveMap;
|
||||
use amdgpu_sysfs::gpu_handle::PerformanceLevel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(tag = "command", content = "args", rename_all = "snake_case")]
|
||||
pub enum Request<'a> {
|
||||
Ping,
|
||||
ListDevices,
|
||||
SystemInfo,
|
||||
DeviceInfo {
|
||||
id: &'a str,
|
||||
},
|
||||
DeviceStats {
|
||||
id: &'a str,
|
||||
},
|
||||
DeviceClocksInfo {
|
||||
id: &'a str,
|
||||
},
|
||||
SetFanControl {
|
||||
id: &'a str,
|
||||
enabled: bool,
|
||||
curve: Option<FanCurveMap>,
|
||||
},
|
||||
SetPowerCap {
|
||||
id: &'a str,
|
||||
cap: Option<f64>,
|
||||
},
|
||||
SetPerformanceLevel {
|
||||
id: &'a str,
|
||||
performance_level: PerformanceLevel,
|
||||
},
|
||||
SetClocksValue {
|
||||
id: &'a str,
|
||||
command: SetClocksCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
|
||||
pub enum SetClocksCommand {
|
||||
MaxCoreClock(u32),
|
||||
MaxMemoryClock(u32),
|
||||
MaxVoltage(u32),
|
||||
Reset,
|
||||
}
|
8
lact-schema/src/response.rs
Normal file
8
lact-schema/src/response.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "status", content = "data", rename_all = "snake_case")]
|
||||
pub enum Response<T> {
|
||||
Ok(T),
|
||||
Error(String),
|
||||
}
|
45
lact-schema/src/tests.rs
Normal file
45
lact-schema/src/tests.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use crate::{Pong, Request, Response};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn ping_requset() {
|
||||
let value = r#"{
|
||||
"command": "ping"
|
||||
}"#;
|
||||
let request: Request = serde_json::from_str(value).unwrap();
|
||||
|
||||
assert_eq!(request, Request::Ping);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pong_response() {
|
||||
let expected_response = json!({
|
||||
"status": "ok",
|
||||
"data": null
|
||||
});
|
||||
let response = Response::Ok(Pong);
|
||||
|
||||
assert_eq!(serde_json::to_value(&response).unwrap(), expected_response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controllers_response() {
|
||||
let expected_response = json!({
|
||||
"status": "ok",
|
||||
"data": ["1002:67DF-1DA2:E387-0000:0f:00.0"]
|
||||
});
|
||||
let response = Response::Ok(vec!["1002:67DF-1DA2:E387-0000:0f:00.0"]);
|
||||
assert_eq!(serde_json::to_value(response).unwrap(), expected_response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_response() {
|
||||
let expected_response = json!({
|
||||
"status": "error",
|
||||
"data": "my super error"
|
||||
});
|
||||
|
||||
let response = Response::<()>::Error("my super error".to_owned());
|
||||
|
||||
assert_eq!(serde_json::to_value(&response).unwrap(), expected_response);
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=LACT
|
||||
Description=AMDGPU Control Application
|
||||
Exec=lact-gui
|
14
lact/Cargo.toml
Normal file
14
lact/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "lact"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["lact-gui"]
|
||||
|
||||
[dependencies]
|
||||
lact-daemon = { path = "../lact-daemon" }
|
||||
lact-cli = { path = "../lact-cli" }
|
||||
lact-gui = { path = "../lact-gui", optional = true }
|
||||
anyhow = "1.0.69"
|
||||
clap = { version = "4.1.6", features = ["derive"] }
|
17
lact/src/args.rs
Normal file
17
lact/src/args.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use lact_cli::args::CliArgs;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run the daemon
|
||||
Daemon,
|
||||
/// Run the GUI
|
||||
Gui,
|
||||
Cli(CliArgs),
|
||||
}
|
27
lact/src/main.rs
Normal file
27
lact/src/main.rs
Normal file
@ -0,0 +1,27 @@
|
||||
mod args;
|
||||
|
||||
use args::{Args, Command};
|
||||
use clap::Parser;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
let command = args.command.unwrap_or(Command::Gui);
|
||||
|
||||
match command {
|
||||
Command::Daemon => lact_daemon::run(),
|
||||
Command::Gui => run_gui(),
|
||||
Command::Cli(cli_args) => lact_cli::run(cli_args),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lact-gui")]
|
||||
fn run_gui() -> anyhow::Result<()> {
|
||||
lact_gui::run()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "lact-gui"))]
|
||||
fn run_gui() -> anyhow::Result<()> {
|
||||
use anyhow::anyhow;
|
||||
|
||||
Err(anyhow!("LACT was built without GUI support"))
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=AMDGPU Control Daemon
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/lact-daemon
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
35
pkg/README.md
Normal file
35
pkg/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# What is this
|
||||
|
||||
This is a directory used by [pkger](https://github.com/vv9k/pkger/) to generate packages for different distros from a single manifest.
|
||||
|
||||
Usage:
|
||||
```
|
||||
pkger -c .pkger.yml build lact
|
||||
```
|
||||
(Should be ran in the repo root).
|
||||
|
||||
Generated packages will be placed in `pkg/output`.
|
||||
|
||||
# Why is there no AppImage/Flatpak/Docker?
|
||||
|
||||
Unfortunately, due to the nature of the app that has 2 parts (daemon running as root and a GUI running as a user), none of the popular universal formats fit it very well.
|
||||
|
||||
---
|
||||
Flatpak is mainly designed for graphical desktop applications, with support for user CLI apps as well.
|
||||
|
||||
It does not support services, and running a Flatpak as root is also problematic. This means that the LACT daemon can't run as Flatpak.
|
||||
|
||||
---
|
||||
AppImage on the other hand can run as root. However it is also mainly designed for graphical apps where you download a run a single file.
|
||||
|
||||
This means that you would need to manually install a service file alongside the AppImage, though this could be automated with a script.
|
||||
|
||||
The bigger problem is that AppImages are built by taking the dependencies of the host system. Some things (primarly `glibc`) are usually not bundled with the image, which means that the recommended way to package things is by building the AppImage on the oldest possible system that you want your app to run on. This is not really possible with LACT, as it uses gtk4, which means that the "universal" AppImage would only run on modern distros anyway.
|
||||
|
||||
It is possible to bundle glibc with the AppImage by using [appimage-builder](https://appimage-builder.readthedocs.io/), which would enable the image to run on older systems. The problem is that the resulting file ends up being very large, and since AppImage extract itself on startup it means that it takes about 10 seconds to start every time even on a relatively powerful system. There are flags that allow AppImages to reuse files between restarts, but it ends up storing hundreds of megabytes of data on /tmp (which is stored in RAM on most systems). Overall it is not a very good user experience.
|
||||
|
||||
---
|
||||
The daemon could run in a Docker container with little issue. However running graphical applications in Docker, while possible, is extremely inconvenient and doesn't integrate with the rest of the system.
|
||||
|
||||
---
|
||||
All of this means that native packaging is currently the only feasible way to distribute LACT.
|
1
pkg/images/debian-12/Dockerfile
Normal file
1
pkg/images/debian-12/Dockerfile
Normal file
@ -0,0 +1 @@
|
||||
FROM debian:bookworm
|
1
pkg/images/fedora-37/Dockerfile
Normal file
1
pkg/images/fedora-37/Dockerfile
Normal file
@ -0,0 +1 @@
|
||||
FROM fedora:37
|
1
pkg/images/ubuntu-2204/Dockerfile
Normal file
1
pkg/images/ubuntu-2204/Dockerfile
Normal file
@ -0,0 +1 @@
|
||||
FROM ubuntu:jammy
|
33
pkg/recipes/lact/recipe.yml
Normal file
33
pkg/recipes/lact/recipe.yml
Normal file
@ -0,0 +1,33 @@
|
||||
metadata:
|
||||
name: lact
|
||||
description: AMDGPU control utility
|
||||
arch: x86_64
|
||||
license: MIT
|
||||
version: 0.2.0
|
||||
maintainer: ilya-zlobintsev
|
||||
url: https://github.com/ilya-zlobintsev/lact
|
||||
source:
|
||||
- '../../..'
|
||||
provides: [ lact ]
|
||||
depends:
|
||||
all: [ hwdata ]
|
||||
debian-12+ubuntu-2204: [ libgtk-4-1 ]
|
||||
fedora-37: [ gtk4 ]
|
||||
build_depends:
|
||||
all: [ curl, make ]
|
||||
debian-12+ubuntu-2204: [ libgtk-4-dev, pkg-config, build-essential ]
|
||||
fedora-37: [ gtk4-devel, gcc ]
|
||||
all_images: true
|
||||
env:
|
||||
RUSTUP_URL: https://sh.rustup.rs
|
||||
configure:
|
||||
steps:
|
||||
- cmd: curl -o /tmp/install_rust.sh $RUSTUP_URL
|
||||
- cmd: sh /tmp/install_rust.sh -y --default-toolchain stable
|
||||
build:
|
||||
steps:
|
||||
- cmd: bash -c "source $HOME/.cargo/env && make"
|
||||
install:
|
||||
steps:
|
||||
- cmd: bash -c "cd $PKGER_BLD_DIR && DESTDIR=$PKGER_OUT_DIR/usr make install"
|
||||
|
7
res/io.github.lact-linux.desktop
Normal file
7
res/io.github.lact-linux.desktop
Normal file
@ -0,0 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=LACT
|
||||
GenericName=AMDGPU Control Application
|
||||
Exec=lact gui
|
||||
Icon=io.github.lact-linux
|
||||
Categories=System;
|
BIN
res/io.github.lact-linux.png
Normal file
BIN
res/io.github.lact-linux.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
2
deb/usr/lib/systemd/system/lactd.service → res/lactd.service
Executable file → Normal file
2
deb/usr/lib/systemd/system/lactd.service → res/lactd.service
Executable file → Normal file
@ -3,7 +3,7 @@ Description=AMDGPU Control Daemon
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/lact-daemon
|
||||
ExecStart=lact daemon
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
Loading…
Reference in New Issue
Block a user