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)?,
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}