i3status_rs/blocks/
amd_gpu.rs1use std::path::PathBuf;
39use std::str::FromStr;
40
41use tokio::fs::read_dir;
42
43use super::prelude::*;
44use crate::util::read_file;
45
46#[derive(Deserialize, Debug, SmartDefault)]
47#[serde(default)]
48pub struct Config {
49 pub device: Option<String>,
50 #[serde(flatten)]
51 pub formats: MaybeMultiFormatConfig,
52 #[default(5.into())]
53 pub interval: Seconds,
54}
55
56pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
57 let declare = |output: OutputPlan| output.icon("icon", IconChoices::one(icons::GPU));
60 let formats = config.formats.with_default(" $icon $utilization ")?;
61 BlockPlan::new(format_outputs(formats, declare))
62}
63
64pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
65 let mut actions = api.get_actions()?;
66 api.set_default_actions(&[
67 (MouseButton::Left, None, "next_format"),
68 (MouseButton::Right, None, "prev_format"),
69 ])?;
70
71 let mut formats = FormatRotation::new(plan)?;
72
73 let device = match &config.device {
74 Some(name) => Device::new(name).await?,
75 None => Device::default_card()
76 .await
77 .error("failed to get default GPU")?
78 .error("no GPU found")?,
79 };
80
81 loop {
82 let output = formats.current();
83 let mut widget = output.new_widget();
84
85 let info = device.read_info().await?;
86
87 widget.set_values(map! {
88 "icon" => Value::icon(icons::GPU),
89 "utilization" => Value::percents(info.utilization_percents),
90 "vram_total" => Value::bytes(info.vram_total_bytes),
91 "vram_used" => Value::bytes(info.vram_used_bytes),
92 "vram_used_percents" => Value::percents(info.vram_used_bytes / info.vram_total_bytes * 100.0),
93 });
94
95 widget.state = match info.utilization_percents {
96 x if x > 90.0 => State::Critical,
97 x if x > 60.0 => State::Warning,
98 x if x > 30.0 => State::Info,
99 _ => State::Idle,
100 };
101
102 api.set_widget(widget)?;
103
104 loop {
105 select! {
106 _ = sleep(config.interval.0) => break,
107 _ = api.wait_for_update_request() => break,
108 Some(action) = actions.recv() => match action.as_ref() {
109 "next_format" | "toggle_format" => {
110 formats.next();
111 break;
112 }
113 "prev_format" => {
114 formats.prev();
115 break;
116 }
117 _ => (),
118 }
119 }
120 }
121 }
122}
123
124pub struct Device {
125 path: PathBuf,
126}
127
128struct GpuInfo {
129 utilization_percents: f64,
130 vram_total_bytes: f64,
131 vram_used_bytes: f64,
132}
133
134impl Device {
135 async fn new(name: &str) -> Result<Self, Error> {
136 let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
137
138 if !tokio::fs::try_exists(&path)
139 .await
140 .error("Unable to stat file")?
141 {
142 Err(Error::new(format!("Device {name} not found")))
143 } else {
144 Ok(Self { path })
145 }
146 }
147
148 async fn default_card() -> std::io::Result<Option<Self>> {
149 let mut dir = read_dir("/sys/class/drm").await?;
150
151 while let Some(entry) = dir.next_entry().await? {
152 let name = entry.file_name();
153 let Some(name) = name.to_str() else { continue };
154 if !name.starts_with("card") {
155 continue;
156 }
157
158 let mut path = entry.path();
159 path.push("device");
160
161 if let Ok(uevent) = read_file(path.join("uevent")).await
162 && uevent.contains("PCI_ID=1002")
163 {
164 return Ok(Some(Self { path }));
165 }
166 }
167
168 Ok(None)
169 }
170
171 async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
172 read_file(self.path.join(prop))
173 .await
174 .ok()
175 .and_then(|x| x.parse().ok())
176 }
177
178 async fn read_info(&self) -> Result<GpuInfo> {
179 Ok(GpuInfo {
180 utilization_percents: self
181 .read_prop::<f64>("gpu_busy_percent")
182 .await
183 .error("Failed to read gpu_busy_percent")?,
184 vram_total_bytes: self
185 .read_prop::<f64>("mem_info_vram_total")
186 .await
187 .error("Failed to read mem_info_vram_total")?,
188 vram_used_bytes: self
189 .read_prop::<f64>("mem_info_vram_used")
190 .await
191 .error("Failed to read mem_info_vram_used")?,
192 })
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[tokio::test]
201 async fn test_non_existing_gpu_device() {
202 let device = Device::new("/nope").await;
203 assert!(device.is_err());
204 }
205
206 #[test]
207 fn plan_declares_single_format_with_gpu_icon() {
208 let plan = prepare(&Config::default()).unwrap();
209 let format = plan.output("format").unwrap();
210 assert_eq!(format.single_icon("icon").unwrap(), "gpu");
211 assert!(format.format().contains_key("utilization"));
212 assert!(plan.output("format2").is_err());
213 }
214
215 #[test]
216 fn every_configured_format_is_declared() {
217 let config: Config =
218 toml::from_str(r#"format = [" $icon $utilization ", " $icon $vram_used_percents "]"#)
219 .unwrap();
220 let plan = prepare(&config).unwrap();
221 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
222 assert_eq!(ids, ["format", "format2"]);
223 let second = plan.output("format2").unwrap();
224 assert!(second.format().contains_key("vram_used_percents"));
225 assert_eq!(second.single_icon("icon").unwrap(), "gpu");
226 }
227}