Skip to main content

i3status_rs/blocks/
amd_gpu.rs

1//! Display the stats of your AMD GPU
2//!
3//! # Configuration
4//!
5//! Key | Values | Default
6//! ----|--------|--------
7//! `device` | The device in `/sys/class/drm/` to read from. | Any AMD card
8//! `format` | A [MultiFormat][MaybeMultiFormatConfig] string to customise the output of this block. See below for available placeholders. | `[" $icon $utilization "]`
9//! `interval` | Update interval in seconds | `5`
10//!
11//! Placeholder          | Value                               | Type   | Unit
12//! ---------------------|-------------------------------------|--------|------------
13//! `icon`               | A static icon                       | Icon   | -
14//! `utilization`        | GPU utilization                     | Number | %
15//! `vram_total`         | Total VRAM                          | Number | Bytes
16//! `vram_used`          | Used VRAM                           | Number | Bytes
17//! `vram_used_percents` | Used VRAM / Total VRAM              | Number | %
18//!
19//! Action          | Description                               | Default button
20//! ----------------|-------------------------------------------|---------------
21//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
22//! `next_format`  | Switches to the next format in the list     | Left
23//! `prev_format`  | Switches to the previous format in the list | Right
24//!
25//! # Example
26//!
27//! ```toml
28//! [[block]]
29//! block = "amd_gpu"
30//! format = " $icon $utilization "
31//! format_alt = " $icon MEM: $vram_used_percents ($vram_used/$vram_total) "
32//! interval = 1
33//! ```
34//!
35//! # Icons Used
36//! - `gpu`
37
38use std::path::PathBuf;
39use std::str::FromStr;
40
41use tokio::fs::read_dir;
42
43use super::prelude::*;
44use crate::util::read_file;
45
46#[derive(Deserialize, Debug, SmartDefault)]
47#[serde(default)]
48pub struct Config {
49    pub device: Option<String>,
50    #[serde(flatten)]
51    pub formats: MaybeMultiFormatConfig,
52    #[default(5.into())]
53    pub interval: Seconds,
54}
55
56pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
57    let mut actions = api.get_actions()?;
58    api.set_default_actions(&[
59        (MouseButton::Left, None, "next_format"),
60        (MouseButton::Right, None, "prev_format"),
61    ])?;
62
63    let mut formats = config.formats.with_default(" $icon $utilization ")?;
64
65    let device = match &config.device {
66        Some(name) => Device::new(name).await?,
67        None => Device::default_card()
68            .await
69            .error("failed to get default GPU")?
70            .error("no GPU found")?,
71    };
72
73    loop {
74        let mut widget = Widget::new().with_format(formats.get_format());
75
76        let info = device.read_info().await?;
77
78        widget.set_values(map! {
79            "icon" => Value::icon("gpu"),
80            "utilization" => Value::percents(info.utilization_percents),
81            "vram_total" => Value::bytes(info.vram_total_bytes),
82            "vram_used" => Value::bytes(info.vram_used_bytes),
83            "vram_used_percents" => Value::percents(info.vram_used_bytes / info.vram_total_bytes * 100.0),
84        });
85
86        widget.state = match info.utilization_percents {
87            x if x > 90.0 => State::Critical,
88            x if x > 60.0 => State::Warning,
89            x if x > 30.0 => State::Info,
90            _ => State::Idle,
91        };
92
93        api.set_widget(widget)?;
94
95        loop {
96            select! {
97                _ = sleep(config.interval.0) => break,
98                _ = api.wait_for_update_request() => break,
99                Some(action) = actions.recv() => match action.as_ref() {
100                    "next_format" | "toggle_format" => {
101                        formats.next_format();
102                        break;
103                    }
104                    "prev_format" => {
105                        formats.prev_format();
106                        break;
107                    }
108                    _ => (),
109                }
110            }
111        }
112    }
113}
114
115pub struct Device {
116    path: PathBuf,
117}
118
119struct GpuInfo {
120    utilization_percents: f64,
121    vram_total_bytes: f64,
122    vram_used_bytes: f64,
123}
124
125impl Device {
126    async fn new(name: &str) -> Result<Self, Error> {
127        let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
128
129        if !tokio::fs::try_exists(&path)
130            .await
131            .error("Unable to stat file")?
132        {
133            Err(Error::new(format!("Device {name} not found")))
134        } else {
135            Ok(Self { path })
136        }
137    }
138
139    async fn default_card() -> std::io::Result<Option<Self>> {
140        let mut dir = read_dir("/sys/class/drm").await?;
141
142        while let Some(entry) = dir.next_entry().await? {
143            let name = entry.file_name();
144            let Some(name) = name.to_str() else { continue };
145            if !name.starts_with("card") {
146                continue;
147            }
148
149            let mut path = entry.path();
150            path.push("device");
151
152            if let Ok(uevent) = read_file(path.join("uevent")).await
153                && uevent.contains("PCI_ID=1002")
154            {
155                return Ok(Some(Self { path }));
156            }
157        }
158
159        Ok(None)
160    }
161
162    async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
163        read_file(self.path.join(prop))
164            .await
165            .ok()
166            .and_then(|x| x.parse().ok())
167    }
168
169    async fn read_info(&self) -> Result<GpuInfo> {
170        Ok(GpuInfo {
171            utilization_percents: self
172                .read_prop::<f64>("gpu_busy_percent")
173                .await
174                .error("Failed to read gpu_busy_percent")?,
175            vram_total_bytes: self
176                .read_prop::<f64>("mem_info_vram_total")
177                .await
178                .error("Failed to read mem_info_vram_total")?,
179            vram_used_bytes: self
180                .read_prop::<f64>("mem_info_vram_used")
181                .await
182                .error("Failed to read mem_info_vram_used")?,
183        })
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[tokio::test]
192    async fn test_non_existing_gpu_device() {
193        let device = Device::new("/nope").await;
194        assert!(device.is_err());
195    }
196}