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 string to customise the output of this block. See below for available placeholders. | `" $icon $utilization "`
9//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click | `None`
10//! `interval` | Update interval in seconds | `5`
11//!
12//! Placeholder          | Value                               | Type   | Unit
13//! ---------------------|-------------------------------------|--------|------------
14//! `icon`               | A static icon                       | Icon   | -
15//! `utilization`        | GPU utilization                     | Number | %
16//! `vram_total`         | Total VRAM                          | Number | Bytes
17//! `vram_used`          | Used VRAM                           | Number | Bytes
18//! `vram_used_percents` | Used VRAM / Total VRAM              | Number | %
19//!
20//! Action          | Description                               | Default button
21//! ----------------|-------------------------------------------|---------------
22//! `toggle_format` | Toggles between `format` and `format_alt` | Left
23//!
24//! # Example
25//!
26//! ```toml
27//! [[block]]
28//! block = "amd_gpu"
29//! format = " $icon $utilization "
30//! format_alt = " $icon MEM: $vram_used_percents ($vram_used/$vram_total) "
31//! interval = 1
32//! ```
33//!
34//! # Icons Used
35//! - `gpu`
36
37use std::path::PathBuf;
38use std::str::FromStr;
39
40use tokio::fs::read_dir;
41
42use super::prelude::*;
43use crate::util::read_file;
44
45#[derive(Deserialize, Debug, SmartDefault)]
46#[serde(deny_unknown_fields, default)]
47pub struct Config {
48    pub device: Option<String>,
49    pub format: FormatConfig,
50    pub format_alt: Option<FormatConfig>,
51    #[default(5.into())]
52    pub interval: Seconds,
53}
54
55pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
56    let mut actions = api.get_actions()?;
57    api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
58
59    let mut format = config.format.with_default(" $icon $utilization ")?;
60    let mut format_alt = match &config.format_alt {
61        Some(f) => Some(f.with_default("")?),
62        None => None,
63    };
64
65    let device = match &config.device {
66        Some(name) => Device::new(name)?,
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(format.clone());
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                    "toggle_format" => {
101                        if let Some(ref mut format_alt) = format_alt {
102                            std::mem::swap(format_alt, &mut format);
103                            break;
104                        }
105                    }
106                    _ => (),
107                }
108            }
109        }
110    }
111}
112
113pub struct Device {
114    path: PathBuf,
115}
116
117struct GpuInfo {
118    utilization_percents: f64,
119    vram_total_bytes: f64,
120    vram_used_bytes: f64,
121}
122
123impl Device {
124    fn new(name: &str) -> Result<Self, Error> {
125        let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
126
127        if !path.exists() {
128            Err(Error::new(format!("Device {name} not found")))
129        } else {
130            Ok(Self { path })
131        }
132    }
133
134    async fn default_card() -> std::io::Result<Option<Self>> {
135        let mut dir = read_dir("/sys/class/drm").await?;
136
137        while let Some(entry) = dir.next_entry().await? {
138            let name = entry.file_name();
139            let Some(name) = name.to_str() else { continue };
140            if !name.starts_with("card") {
141                continue;
142            }
143
144            let mut path = entry.path();
145            path.push("device");
146
147            if let Ok(uevent) = read_file(path.join("uevent")).await
148                && uevent.contains("PCI_ID=1002")
149            {
150                return Ok(Some(Self { path }));
151            }
152        }
153
154        Ok(None)
155    }
156
157    async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
158        read_file(self.path.join(prop))
159            .await
160            .ok()
161            .and_then(|x| x.parse().ok())
162    }
163
164    async fn read_info(&self) -> Result<GpuInfo> {
165        Ok(GpuInfo {
166            utilization_percents: self
167                .read_prop::<f64>("gpu_busy_percent")
168                .await
169                .error("Failed to read gpu_busy_percent")?,
170            vram_total_bytes: self
171                .read_prop::<f64>("mem_info_vram_total")
172                .await
173                .error("Failed to read mem_info_vram_total")?,
174            vram_used_bytes: self
175                .read_prop::<f64>("mem_info_vram_used")
176                .await
177                .error("Failed to read mem_info_vram_used")?,
178        })
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_non_existing_gpu_device() {
188        let device = Device::new("/nope");
189        assert!(device.is_err());
190    }
191}