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 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}