1use bevy::prelude::*;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Resource, Serialize, Deserialize)]
6pub struct MachineInfo {
7 pub os: String,
8 pub os_version: String,
9 pub kernel_version: String,
10 pub hostname: String,
11 pub cpu_count: usize,
12 pub total_memory_bytes: u64,
13 pub git_version: String,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub gpu_info: Option<GpuInfo>,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
20pub struct GpuInfo {
21 pub name: String,
22 pub vendor: u32,
23 pub device: u32,
24 pub device_type: String,
25 pub backend: String,
26 pub driver: String,
27 pub driver_info: String,
28}
29
30#[cfg(feature = "bevy_render")]
31use {
32 bevy::render::{
33 Render, RenderApp,
34 renderer::{RenderAdapter, RenderAdapterInfo},
35 },
36 std::ops::Deref,
37 std::sync::{Arc, Mutex},
38};
39
40#[cfg(feature = "bevy_render")]
41#[derive(Clone, Resource, Default)]
42struct SharedGpuInfo(Arc<Mutex<Option<GpuInfo>>>);
43
44#[cfg(feature = "bevy_render")]
45#[derive(Resource, Default)]
46struct GpuSyncRegistered;
47
48#[cfg(feature = "bevy_render")]
49impl SharedGpuInfo {
50 fn get(&self) -> Option<GpuInfo> {
51 self.0.lock().ok().and_then(|guard| guard.clone())
52 }
53
54 fn set(&self, info: GpuInfo) {
55 if let Ok(mut guard) = self.0.lock() {
56 *guard = Some(info);
57 }
58 }
59}
60
61impl MachineInfo {
62 pub fn collect(world: &World) -> Self {
65 let git_version = get_git_version();
66
67 let gpu_info = collect_gpu_info(world);
69
70 use sysinfo::System;
71 let sys = System::new_all();
72
73 let mut machine_info = Self {
74 os: System::name().unwrap_or_else(|| "Unknown".to_string()),
75 os_version: System::os_version().unwrap_or_else(|| "Unknown".to_string()),
76 kernel_version: System::kernel_version().unwrap_or_else(|| "Unknown".to_string()),
77 hostname: System::host_name().unwrap_or_else(|| "Unknown".to_string()),
78 cpu_count: sys.cpus().len(),
79 total_memory_bytes: sys.total_memory() * 1024,
80 git_version,
81 gpu_info,
82 };
83
84 #[cfg(feature = "sysinfo_plugin")]
85 {
86 use bevy::diagnostic::SystemInfo;
87
88 if let Some(system_info) = world.get_resource::<SystemInfo>() {
89 if let Some(os) = normalize_system_info_value(&system_info.os) {
90 if machine_info.os.eq_ignore_ascii_case("unknown") {
91 machine_info.os = os.to_string();
92 }
93 if machine_info.os_version.eq_ignore_ascii_case("unknown") {
94 machine_info.os_version = os.to_string();
95 }
96 }
97
98 if let Some(kernel) = normalize_system_info_value(&system_info.kernel) {
99 machine_info.kernel_version = kernel.to_string();
100 }
101
102 if let Some(core_count) = normalize_system_info_value(&system_info.core_count)
103 .and_then(|value| value.parse::<usize>().ok())
104 {
105 machine_info.cpu_count = core_count;
106 }
107
108 if let Some(memory_bytes) =
109 normalize_system_info_value(&system_info.memory).and_then(parse_memory_to_bytes)
110 {
111 machine_info.total_memory_bytes = memory_bytes;
112 }
113 }
114 }
115
116 machine_info
117 }
118}
119
120fn collect_gpu_info(_world: &World) -> Option<GpuInfo> {
122 #[cfg(feature = "bevy_render")]
123 {
124 if let Some(shared) = _world.get_resource::<SharedGpuInfo>()
125 && let Some(info) = shared.get()
126 {
127 return Some(info);
128 }
129
130 if let Some(adapter_info) = _world.get_resource::<RenderAdapterInfo>() {
131 let info = adapter_info.0.deref();
132 return Some(GpuInfo {
133 name: info.name.clone(),
134 vendor: info.vendor,
135 device: info.device,
136 device_type: format!("{:?}", info.device_type),
137 backend: format!("{:?}", info.backend),
138 driver: info.driver.clone(),
139 driver_info: info.driver_info.clone(),
140 });
141 }
142
143 if let Some(render_adapter) = _world.get_resource::<RenderAdapter>() {
144 let info = render_adapter.get_info();
145 return Some(GpuInfo {
146 name: info.name.clone(),
147 vendor: info.vendor,
148 device: info.device,
149 device_type: format!("{:?}", info.device_type),
150 backend: format!("{:?}", info.backend),
151 driver: info.driver.clone(),
152 driver_info: info.driver_info.clone(),
153 });
154 }
155 }
156
157 None
158}
159
160#[cfg(feature = "bevy_render")]
161fn setup_gpu_info_bridge(app: &mut App) {
162 if app.world().contains_resource::<SharedGpuInfo>() {
163 return;
164 }
165
166 let shared = SharedGpuInfo::default();
167 app.insert_resource(shared.clone());
168
169 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
170 render_app.insert_resource(shared);
171 render_app.add_systems(Render, update_shared_gpu_info);
172 }
173}
174
175#[cfg(feature = "bevy_render")]
176fn update_shared_gpu_info(
177 shared: Res<SharedGpuInfo>,
178 adapter_info: Option<Res<RenderAdapterInfo>>,
179) {
180 if let Some(adapter_info) = adapter_info {
181 let info = adapter_info.0.deref();
182 shared.set(GpuInfo {
183 name: info.name.clone(),
184 vendor: info.vendor,
185 device: info.device,
186 device_type: format!("{:?}", info.device_type),
187 backend: format!("{:?}", info.backend),
188 driver: info.driver.clone(),
189 driver_info: info.driver_info.clone(),
190 });
191 }
192}
193
194#[cfg(feature = "bevy_render")]
195fn sync_gpu_info_from_shared(
196 shared: Option<Res<SharedGpuInfo>>,
197 machine_info: Option<ResMut<MachineInfo>>,
198) {
199 let (Some(shared), Some(mut machine_info)) = (shared, machine_info) else {
200 return;
201 };
202
203 if let Some(info) = shared.get()
204 && machine_info.gpu_info.as_ref() != Some(&info)
205 {
206 machine_info.gpu_info = Some(info);
207 }
208}
209
210fn get_git_version() -> String {
212 #[cfg(feature = "git-version")]
213 {
214 git_version::git_version!(
215 args = ["--always", "--dirty=-modified"],
216 fallback = "unknown"
217 )
218 .to_string()
219 }
220
221 #[cfg(not(feature = "git-version"))]
222 {
223 "unknown".to_string()
224 }
225}
226
227pub fn collect_machine_info(app: &mut App) {
229 #[cfg(feature = "bevy_render")]
230 setup_gpu_info_bridge(app);
231
232 let mut machine_info = MachineInfo::collect(app.world());
233
234 #[cfg(feature = "bevy_render")]
235 if machine_info.gpu_info.is_none()
236 && let Some(shared) = app.world().get_resource::<SharedGpuInfo>()
237 && let Some(info) = shared.get()
238 {
239 machine_info.gpu_info = Some(info);
240 }
241
242 info!(
243 "Machine info collected: {} CPUs, {} bytes memory",
244 machine_info.cpu_count, machine_info.total_memory_bytes
245 );
246 if let Some(ref gpu) = machine_info.gpu_info {
247 info!("GPU info collected: {} ({})", gpu.name, gpu.backend);
248 }
249
250 #[cfg(feature = "bevy_render")]
251 {
252 if !app.world().contains_resource::<GpuSyncRegistered>() {
253 app.insert_resource(GpuSyncRegistered);
254 app.add_systems(Update, sync_gpu_info_from_shared);
255 }
256 }
257
258 app.insert_resource(machine_info);
259}
260
261#[cfg(feature = "sysinfo_plugin")]
262fn normalize_system_info_value(value: &str) -> Option<&str> {
263 let trimmed = value.trim();
264 if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("not available") {
265 None
266 } else {
267 Some(trimmed)
268 }
269}
270
271#[cfg(feature = "sysinfo_plugin")]
272fn parse_memory_to_bytes(value: &str) -> Option<u64> {
273 let mut parts = value.split_whitespace();
274 let amount_str = parts.next()?;
275 let unit = parts.next().unwrap_or("bytes");
276
277 let normalized_amount = amount_str.replace(',', "");
278 let amount: f64 = normalized_amount.parse().ok()?;
279
280 const KIB: f64 = 1024.0;
281 const MIB: f64 = KIB * 1024.0;
282 const GIB: f64 = MIB * 1024.0;
283
284 let bytes = match unit {
285 "GiB" => amount * GIB,
286 "MiB" => amount * MIB,
287 "KiB" => amount * KIB,
288 "bytes" | "B" => amount,
289 _ => return None,
290 };
291
292 Some(bytes.round() as u64)
293}