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` (`$icon`)
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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
57    // Every output renders the same value set, built unconditionally on
58    // every update.
59    let declare = |output: OutputPlan| output.icon("icon", IconChoices::one(icons::GPU));
60    let formats = config.formats.with_default(" $icon $utilization ")?;
61    BlockPlan::new(format_outputs(formats, declare))
62}
63
64pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
65    let mut actions = api.get_actions()?;
66    api.set_default_actions(&[
67        (MouseButton::Left, None, "next_format"),
68        (MouseButton::Right, None, "prev_format"),
69    ])?;
70
71    let mut formats = FormatRotation::new(plan)?;
72
73    let device = match &config.device {
74        Some(name) => Device::new(name).await?,
75        None => Device::default_card()
76            .await
77            .error("failed to get default GPU")?
78            .error("no GPU found")?,
79    };
80
81    loop {
82        let output = formats.current();
83        let mut widget = output.new_widget();
84
85        let info = device.read_info().await?;
86
87        widget.set_values(map! {
88            "icon" => Value::icon(icons::GPU),
89            "utilization" => Value::percents(info.utilization_percents),
90            "vram_total" => Value::bytes(info.vram_total_bytes),
91            "vram_used" => Value::bytes(info.vram_used_bytes),
92            "vram_used_percents" => Value::percents(info.vram_used_bytes / info.vram_total_bytes * 100.0),
93        });
94
95        widget.state = match info.utilization_percents {
96            x if x > 90.0 => State::Critical,
97            x if x > 60.0 => State::Warning,
98            x if x > 30.0 => State::Info,
99            _ => State::Idle,
100        };
101
102        api.set_widget(widget)?;
103
104        loop {
105            select! {
106                _ = sleep(config.interval.0) => break,
107                _ = api.wait_for_update_request() => break,
108                Some(action) = actions.recv() => match action.as_ref() {
109                    "next_format" | "toggle_format" => {
110                        formats.next();
111                        break;
112                    }
113                    "prev_format" => {
114                        formats.prev();
115                        break;
116                    }
117                    _ => (),
118                }
119            }
120        }
121    }
122}
123
124pub struct Device {
125    path: PathBuf,
126}
127
128struct GpuInfo {
129    utilization_percents: f64,
130    vram_total_bytes: f64,
131    vram_used_bytes: f64,
132}
133
134impl Device {
135    async fn new(name: &str) -> Result<Self, Error> {
136        let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
137
138        if !tokio::fs::try_exists(&path)
139            .await
140            .error("Unable to stat file")?
141        {
142            Err(Error::new(format!("Device {name} not found")))
143        } else {
144            Ok(Self { path })
145        }
146    }
147
148    async fn default_card() -> std::io::Result<Option<Self>> {
149        let mut dir = read_dir("/sys/class/drm").await?;
150
151        while let Some(entry) = dir.next_entry().await? {
152            let name = entry.file_name();
153            let Some(name) = name.to_str() else { continue };
154            if !name.starts_with("card") {
155                continue;
156            }
157
158            let mut path = entry.path();
159            path.push("device");
160
161            if let Ok(uevent) = read_file(path.join("uevent")).await
162                && uevent.contains("PCI_ID=1002")
163            {
164                return Ok(Some(Self { path }));
165            }
166        }
167
168        Ok(None)
169    }
170
171    async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
172        read_file(self.path.join(prop))
173            .await
174            .ok()
175            .and_then(|x| x.parse().ok())
176    }
177
178    async fn read_info(&self) -> Result<GpuInfo> {
179        Ok(GpuInfo {
180            utilization_percents: self
181                .read_prop::<f64>("gpu_busy_percent")
182                .await
183                .error("Failed to read gpu_busy_percent")?,
184            vram_total_bytes: self
185                .read_prop::<f64>("mem_info_vram_total")
186                .await
187                .error("Failed to read mem_info_vram_total")?,
188            vram_used_bytes: self
189                .read_prop::<f64>("mem_info_vram_used")
190                .await
191                .error("Failed to read mem_info_vram_used")?,
192        })
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[tokio::test]
201    async fn test_non_existing_gpu_device() {
202        let device = Device::new("/nope").await;
203        assert!(device.is_err());
204    }
205
206    #[test]
207    fn plan_declares_single_format_with_gpu_icon() {
208        let plan = prepare(&Config::default()).unwrap();
209        let format = plan.output("format").unwrap();
210        assert_eq!(format.single_icon("icon").unwrap(), "gpu");
211        assert!(format.format().contains_key("utilization"));
212        assert!(plan.output("format2").is_err());
213    }
214
215    #[test]
216    fn every_configured_format_is_declared() {
217        let config: Config =
218            toml::from_str(r#"format = [" $icon $utilization ", " $icon $vram_used_percents "]"#)
219                .unwrap();
220        let plan = prepare(&config).unwrap();
221        let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
222        assert_eq!(ids, ["format", "format2"]);
223        let second = plan.output("format2").unwrap();
224        assert!(second.format().contains_key("vram_used_percents"));
225        assert_eq!(second.single_icon("icon").unwrap(), "gpu");
226    }
227}