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            let Ok(uevent) = read_file(path.join("uevent")).await else {
148                continue;
149            };
150
151            if uevent.contains("PCI_ID=1002") {
152                return Ok(Some(Self { path }));
153            }
154        }
155
156        Ok(None)
157    }
158
159    async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
160        read_file(self.path.join(prop))
161            .await
162            .ok()
163            .and_then(|x| x.parse().ok())
164    }
165
166    async fn read_info(&self) -> Result<GpuInfo> {
167        Ok(GpuInfo {
168            utilization_percents: self
169                .read_prop::<f64>("gpu_busy_percent")
170                .await
171                .error("Failed to read gpu_busy_percent")?,
172            vram_total_bytes: self
173                .read_prop::<f64>("mem_info_vram_total")
174                .await
175                .error("Failed to read mem_info_vram_total")?,
176            vram_used_bytes: self
177                .read_prop::<f64>("mem_info_vram_used")
178                .await
179                .error("Failed to read mem_info_vram_used")?,
180        })
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_non_existing_gpu_device() {
190        let device = Device::new("/nope");
191        assert!(device.is_err());
192    }
193}