use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Resource, Serialize, Deserialize)]
pub struct MachineInfo {
pub os: String,
pub os_version: String,
pub kernel_version: String,
pub hostname: String,
pub cpu_count: usize,
pub total_memory_bytes: u64,
pub git_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gpu_info: Option<GpuInfo>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct GpuInfo {
pub name: String,
pub vendor: u32,
pub device: u32,
pub device_type: String,
pub backend: String,
pub driver: String,
pub driver_info: String,
}
#[cfg(feature = "bevy_render")]
use {
bevy::render::{
Render, RenderApp,
renderer::{RenderAdapter, RenderAdapterInfo},
},
std::ops::Deref,
std::sync::{Arc, Mutex},
};
#[cfg(feature = "bevy_render")]
#[derive(Clone, Resource, Default)]
struct SharedGpuInfo(Arc<Mutex<Option<GpuInfo>>>);
#[cfg(feature = "bevy_render")]
#[derive(Resource, Default)]
struct GpuSyncRegistered;
#[cfg(feature = "bevy_render")]
impl SharedGpuInfo {
fn get(&self) -> Option<GpuInfo> {
self.0.lock().ok().and_then(|guard| guard.clone())
}
fn set(&self, info: GpuInfo) {
if let Ok(mut guard) = self.0.lock() {
*guard = Some(info);
}
}
}
impl MachineInfo {
pub fn collect(world: &World) -> Self {
let git_version = get_git_version();
let gpu_info = collect_gpu_info(world);
use sysinfo::System;
let sys = System::new_all();
let mut machine_info = Self {
os: System::name().unwrap_or_else(|| "Unknown".to_string()),
os_version: System::os_version().unwrap_or_else(|| "Unknown".to_string()),
kernel_version: System::kernel_version().unwrap_or_else(|| "Unknown".to_string()),
hostname: System::host_name().unwrap_or_else(|| "Unknown".to_string()),
cpu_count: sys.cpus().len(),
total_memory_bytes: sys.total_memory() * 1024,
git_version,
gpu_info,
};
#[cfg(feature = "sysinfo_plugin")]
{
use bevy::diagnostic::SystemInfo;
if let Some(system_info) = world.get_resource::<SystemInfo>() {
if let Some(os) = normalize_system_info_value(&system_info.os) {
if machine_info.os.eq_ignore_ascii_case("unknown") {
machine_info.os = os.to_string();
}
if machine_info.os_version.eq_ignore_ascii_case("unknown") {
machine_info.os_version = os.to_string();
}
}
if let Some(kernel) = normalize_system_info_value(&system_info.kernel) {
machine_info.kernel_version = kernel.to_string();
}
if let Some(core_count) = normalize_system_info_value(&system_info.core_count)
.and_then(|value| value.parse::<usize>().ok())
{
machine_info.cpu_count = core_count;
}
if let Some(memory_bytes) =
normalize_system_info_value(&system_info.memory).and_then(parse_memory_to_bytes)
{
machine_info.total_memory_bytes = memory_bytes;
}
}
}
machine_info
}
}
fn collect_gpu_info(_world: &World) -> Option<GpuInfo> {
#[cfg(feature = "bevy_render")]
{
if let Some(shared) = _world.get_resource::<SharedGpuInfo>()
&& let Some(info) = shared.get()
{
return Some(info);
}
if let Some(adapter_info) = _world.get_resource::<RenderAdapterInfo>() {
let info = adapter_info.0.deref();
return Some(GpuInfo {
name: info.name.clone(),
vendor: info.vendor,
device: info.device,
device_type: format!("{:?}", info.device_type),
backend: format!("{:?}", info.backend),
driver: info.driver.clone(),
driver_info: info.driver_info.clone(),
});
}
if let Some(render_adapter) = _world.get_resource::<RenderAdapter>() {
let info = render_adapter.get_info();
return Some(GpuInfo {
name: info.name.clone(),
vendor: info.vendor,
device: info.device,
device_type: format!("{:?}", info.device_type),
backend: format!("{:?}", info.backend),
driver: info.driver.clone(),
driver_info: info.driver_info.clone(),
});
}
}
None
}
#[cfg(feature = "bevy_render")]
fn setup_gpu_info_bridge(app: &mut App) {
if app.world().contains_resource::<SharedGpuInfo>() {
return;
}
let shared = SharedGpuInfo::default();
app.insert_resource(shared.clone());
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.insert_resource(shared);
render_app.add_systems(Render, update_shared_gpu_info);
}
}
#[cfg(feature = "bevy_render")]
fn update_shared_gpu_info(
shared: Res<SharedGpuInfo>,
adapter_info: Option<Res<RenderAdapterInfo>>,
) {
if let Some(adapter_info) = adapter_info {
let info = adapter_info.0.deref();
shared.set(GpuInfo {
name: info.name.clone(),
vendor: info.vendor,
device: info.device,
device_type: format!("{:?}", info.device_type),
backend: format!("{:?}", info.backend),
driver: info.driver.clone(),
driver_info: info.driver_info.clone(),
});
}
}
#[cfg(feature = "bevy_render")]
fn sync_gpu_info_from_shared(
shared: Option<Res<SharedGpuInfo>>,
machine_info: Option<ResMut<MachineInfo>>,
) {
let (Some(shared), Some(mut machine_info)) = (shared, machine_info) else {
return;
};
if let Some(info) = shared.get()
&& machine_info.gpu_info.as_ref() != Some(&info)
{
machine_info.gpu_info = Some(info);
}
}
fn get_git_version() -> String {
#[cfg(feature = "git-version")]
{
git_version::git_version!(
args = ["--always", "--dirty=-modified"],
fallback = "unknown"
)
.to_string()
}
#[cfg(not(feature = "git-version"))]
{
"unknown".to_string()
}
}
pub fn collect_machine_info(app: &mut App) {
#[cfg(feature = "bevy_render")]
setup_gpu_info_bridge(app);
let mut machine_info = MachineInfo::collect(app.world());
#[cfg(feature = "bevy_render")]
if machine_info.gpu_info.is_none()
&& let Some(shared) = app.world().get_resource::<SharedGpuInfo>()
&& let Some(info) = shared.get()
{
machine_info.gpu_info = Some(info);
}
info!(
"Machine info collected: {} CPUs, {} bytes memory",
machine_info.cpu_count, machine_info.total_memory_bytes
);
if let Some(ref gpu) = machine_info.gpu_info {
info!("GPU info collected: {} ({})", gpu.name, gpu.backend);
}
#[cfg(feature = "bevy_render")]
{
if !app.world().contains_resource::<GpuSyncRegistered>() {
app.insert_resource(GpuSyncRegistered);
app.add_systems(Update, sync_gpu_info_from_shared);
}
}
app.insert_resource(machine_info);
}
#[cfg(feature = "sysinfo_plugin")]
fn normalize_system_info_value(value: &str) -> Option<&str> {
let trimmed = value.trim();
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("not available") {
None
} else {
Some(trimmed)
}
}
#[cfg(feature = "sysinfo_plugin")]
fn parse_memory_to_bytes(value: &str) -> Option<u64> {
let mut parts = value.split_whitespace();
let amount_str = parts.next()?;
let unit = parts.next().unwrap_or("bytes");
let normalized_amount = amount_str.replace(',', "");
let amount: f64 = normalized_amount.parse().ok()?;
const KIB: f64 = 1024.0;
const MIB: f64 = KIB * 1024.0;
const GIB: f64 = MIB * 1024.0;
let bytes = match unit {
"GiB" => amount * GIB,
"MiB" => amount * MIB,
"KiB" => amount * KIB,
"bytes" | "B" => amount,
_ => return None,
};
Some(bytes.round() as u64)
}