i3status_rs/blocks/
amd_gpu.rs1use 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).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(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 async fn new(name: &str) -> Result<Self, Error> {
125 let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
126
127 if !tokio::fs::try_exists(&path)
128 .await
129 .error("Unable to stat file")?
130 {
131 Err(Error::new(format!("Device {name} not found")))
132 } else {
133 Ok(Self { path })
134 }
135 }
136
137 async fn default_card() -> std::io::Result<Option<Self>> {
138 let mut dir = read_dir("/sys/class/drm").await?;
139
140 while let Some(entry) = dir.next_entry().await? {
141 let name = entry.file_name();
142 let Some(name) = name.to_str() else { continue };
143 if !name.starts_with("card") {
144 continue;
145 }
146
147 let mut path = entry.path();
148 path.push("device");
149
150 if let Ok(uevent) = read_file(path.join("uevent")).await
151 && uevent.contains("PCI_ID=1002")
152 {
153 return Ok(Some(Self { path }));
154 }
155 }
156
157 Ok(None)
158 }
159
160 async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
161 read_file(self.path.join(prop))
162 .await
163 .ok()
164 .and_then(|x| x.parse().ok())
165 }
166
167 async fn read_info(&self) -> Result<GpuInfo> {
168 Ok(GpuInfo {
169 utilization_percents: self
170 .read_prop::<f64>("gpu_busy_percent")
171 .await
172 .error("Failed to read gpu_busy_percent")?,
173 vram_total_bytes: self
174 .read_prop::<f64>("mem_info_vram_total")
175 .await
176 .error("Failed to read mem_info_vram_total")?,
177 vram_used_bytes: self
178 .read_prop::<f64>("mem_info_vram_used")
179 .await
180 .error("Failed to read mem_info_vram_used")?,
181 })
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[tokio::test]
190 async fn test_non_existing_gpu_device() {
191 let device = Device::new("/nope").await;
192 assert!(device.is_err());
193 }
194}