bevy_fleet/
machine_info.rs

1use bevy::prelude::*;
2use serde::{Deserialize, Serialize};
3
4/// Machine/system information
5#[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/// GPU/Adapter information from WGPU
19#[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    /// Collects machine info from Bevy's SystemInfo resource if available,
63    /// otherwise falls back to sysinfo crate
64    pub fn collect(world: &World) -> Self {
65        let git_version = get_git_version();
66
67        // Try to get GPU info from render adapter
68        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
120/// Collects GPU information from WGPU adapter if available
121fn 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
210/// Gets the git version information
211fn 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
227/// Collects machine information and adds it as a resource
228pub 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}