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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
57 let mut actions = api.get_actions()?;
58 api.set_default_actions(&[
59 (MouseButton::Left, None, "next_format"),
60 (MouseButton::Right, None, "prev_format"),
61 ])?;
62
63 let mut formats = config.formats.with_default(" $icon $utilization ")?;
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(formats.get_format());
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 "next_format" | "toggle_format" => {
101 formats.next_format();
102 break;
103 }
104 "prev_format" => {
105 formats.prev_format();
106 break;
107 }
108 _ => (),
109 }
110 }
111 }
112 }
113}
114
115pub struct Device {
116 path: PathBuf,
117}
118
119struct GpuInfo {
120 utilization_percents: f64,
121 vram_total_bytes: f64,
122 vram_used_bytes: f64,
123}
124
125impl Device {
126 async fn new(name: &str) -> Result<Self, Error> {
127 let path = PathBuf::from(format!("/sys/class/drm/{name}/device"));
128
129 if !tokio::fs::try_exists(&path)
130 .await
131 .error("Unable to stat file")?
132 {
133 Err(Error::new(format!("Device {name} not found")))
134 } else {
135 Ok(Self { path })
136 }
137 }
138
139 async fn default_card() -> std::io::Result<Option<Self>> {
140 let mut dir = read_dir("/sys/class/drm").await?;
141
142 while let Some(entry) = dir.next_entry().await? {
143 let name = entry.file_name();
144 let Some(name) = name.to_str() else { continue };
145 if !name.starts_with("card") {
146 continue;
147 }
148
149 let mut path = entry.path();
150 path.push("device");
151
152 if let Ok(uevent) = read_file(path.join("uevent")).await
153 && uevent.contains("PCI_ID=1002")
154 {
155 return Ok(Some(Self { path }));
156 }
157 }
158
159 Ok(None)
160 }
161
162 async fn read_prop<T: FromStr>(&self, prop: &str) -> Option<T> {
163 read_file(self.path.join(prop))
164 .await
165 .ok()
166 .and_then(|x| x.parse().ok())
167 }
168
169 async fn read_info(&self) -> Result<GpuInfo> {
170 Ok(GpuInfo {
171 utilization_percents: self
172 .read_prop::<f64>("gpu_busy_percent")
173 .await
174 .error("Failed to read gpu_busy_percent")?,
175 vram_total_bytes: self
176 .read_prop::<f64>("mem_info_vram_total")
177 .await
178 .error("Failed to read mem_info_vram_total")?,
179 vram_used_bytes: self
180 .read_prop::<f64>("mem_info_vram_used")
181 .await
182 .error("Failed to read mem_info_vram_used")?,
183 })
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[tokio::test]
192 async fn test_non_existing_gpu_device() {
193 let device = Device::new("/nope").await;
194 assert!(device.is_err());
195 }
196}