2025-04-03 01:37:53 -04:00
|
|
|
use std::collections::HashSet;
|
2025-04-03 01:00:24 -04:00
|
|
|
use std::fmt;
|
|
|
|
use std::str::FromStr;
|
2025-04-10 14:26:52 -04:00
|
|
|
use std::{env, error::Error, path::PathBuf, thread};
|
2025-04-03 01:37:53 -04:00
|
|
|
|
2025-04-10 14:26:52 -04:00
|
|
|
use native_dialog::{MessageDialog, MessageType};
|
|
|
|
use parking_lot::Once;
|
2025-04-03 01:00:24 -04:00
|
|
|
use raw_cpuid::CpuId;
|
2025-04-03 01:37:53 -04:00
|
|
|
use sysinfo::{CpuRefreshKind, RefreshKind, System};
|
2025-04-10 14:26:52 -04:00
|
|
|
use tracing::error;
|
2025-04-03 01:00:24 -04:00
|
|
|
use wgpu::DeviceType;
|
|
|
|
|
|
|
|
mod build_info {
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
|
|
|
}
|
|
|
|
|
2025-04-10 22:57:19 -04:00
|
|
|
static INIT: Once = Once::new();
|
2025-04-10 14:26:52 -04:00
|
|
|
|
|
|
|
pub fn set_panic_hook() {
|
|
|
|
INIT.call_once(|| {
|
|
|
|
let default_hook = std::panic::take_hook();
|
|
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
|
|
if let Err(e) = process_panic(info) {
|
|
|
|
eprintln!("Error in panic hook: {}", e);
|
|
|
|
default_hook(info);
|
|
|
|
}
|
|
|
|
std::process::exit(1);
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Error>> {
|
|
|
|
use std::io::Write;
|
|
|
|
|
|
|
|
use colored::Colorize;
|
|
|
|
|
|
|
|
let log_dir = PathBuf::from_str("./").expect("wtf, The current directory no longer exists?");
|
|
|
|
if !log_dir.exists() {
|
|
|
|
std::fs::create_dir_all(&log_dir)?;
|
|
|
|
}
|
|
|
|
let log_path = log_dir.join("panic.log");
|
|
|
|
|
|
|
|
let mut file = std::fs::File::create(&log_path)?;
|
|
|
|
let payload = info.payload();
|
|
|
|
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
|
|
|
|
*s
|
|
|
|
} else if let Some(s) = payload.downcast_ref::<String>() {
|
|
|
|
s
|
|
|
|
} else {
|
|
|
|
"<non-string panic payload>"
|
|
|
|
};
|
|
|
|
|
|
|
|
writeln!(file, "Panic Occurred: {}", payload_str)?;
|
|
|
|
|
|
|
|
if let Some(location) = info.location() {
|
|
|
|
writeln!(file, "Panic Location: {}", location)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(file, "{}", capture_backtrace().sanitize_path())?;
|
|
|
|
|
|
|
|
// Add more contextual information
|
|
|
|
writeln!(file, "\n--- Additional Information ---")?;
|
|
|
|
|
|
|
|
// Rust Version
|
|
|
|
if let Ok(rust_version) = rust_version() {
|
|
|
|
writeln!(file, "Rust Version: {}", rust_version)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Command-line Arguments
|
|
|
|
writeln!(file, "Command-line Arguments:")?;
|
|
|
|
for arg in env::args() {
|
|
|
|
writeln!(file, " {}", arg)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Environment Variables (consider filtering sensitive ones)
|
|
|
|
writeln!(file, "\nEnvironment Variables (selected):")?;
|
|
|
|
let interesting_env_vars = ["PATH", "RUST_VERSION", "CARGO_TARGET_DIR", "HOME", "USER"];
|
|
|
|
for (key, value) in env::vars() {
|
|
|
|
if interesting_env_vars.contains(&key.as_str()) {
|
|
|
|
writeln!(file, " {}: {}", key, value)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Current Working Directory
|
|
|
|
if let Ok(cwd) = env::current_dir() {
|
|
|
|
writeln!(file, "\nCurrent Working Directory: {}", cwd.display())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Thread Information
|
|
|
|
if let Some(thread) = thread::current().name() {
|
|
|
|
writeln!(file, "\nThread Name: {}", thread)?;
|
|
|
|
} else {
|
|
|
|
writeln!(file, "\nThread ID: {:?}", thread::current().id())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
let panic_msg = format!(
|
|
|
|
r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report.
|
|
|
|
|
|
|
|
We have generated a detailed report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
|
|
|
|
|
|
|
|
To submit the crash report:
|
|
|
|
https://codeberg.org/Caznix/Zenyx/issues
|
|
|
|
We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
|
|
|
|
|
|
|
|
Thank you kindly!"#,
|
|
|
|
log_path.display()
|
|
|
|
);
|
|
|
|
|
|
|
|
let final_msg = format!(
|
|
|
|
r#"{}
|
|
|
|
|
|
|
|
For future reference, the error summary is as follows:
|
|
|
|
{}
|
|
|
|
|
|
|
|
More details can be found in the crash report file."#,
|
|
|
|
panic_msg, payload_str
|
|
|
|
);
|
|
|
|
|
|
|
|
println!("{}", final_msg.red().bold());
|
|
|
|
|
|
|
|
if let Err(e) = MessageDialog::new()
|
|
|
|
.set_type(MessageType::Error)
|
|
|
|
.set_title("A fatal error in Zenyx has occurred")
|
|
|
|
.set_text(&final_msg)
|
|
|
|
.show_confirm()
|
|
|
|
{
|
|
|
|
error!("Failed to show message dialog: {e}")
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rust_version() -> Result<String, Box<dyn Error>> {
|
|
|
|
let version = env!("CARGO_PKG_RUST_VERSION");
|
|
|
|
Ok(version.to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn capture_backtrace() -> String {
|
|
|
|
let mut backtrace = String::new();
|
2025-04-10 22:57:19 -04:00
|
|
|
let sysinfo = SystemMetadata::current();
|
2025-04-10 14:26:52 -04:00
|
|
|
backtrace.push_str(&format!(
|
|
|
|
"--- System Information ---\n{}\n",
|
|
|
|
sysinfo.verbose_summary()
|
|
|
|
));
|
|
|
|
|
|
|
|
let trace = std::backtrace::Backtrace::force_capture();
|
2025-04-10 22:57:19 -04:00
|
|
|
let message = "\n--- Backtrace ---\n\n".to_string();
|
2025-04-10 14:26:52 -04:00
|
|
|
backtrace.push_str(&message);
|
|
|
|
backtrace.push_str(&format!("{trace:#}"));
|
|
|
|
|
|
|
|
backtrace
|
|
|
|
}
|
|
|
|
|
|
|
|
trait Sanitize {
|
|
|
|
fn sanitize_path(&self) -> String;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Sanitize for str {
|
|
|
|
fn sanitize_path(&self) -> String {
|
|
|
|
let prefixes = ["/home/", "/Users/", "\\Users\\", "/opt/home/"];
|
|
|
|
let mut result = String::from(self);
|
|
|
|
|
|
|
|
for prefix in prefixes {
|
|
|
|
if let Some(start_index) = result.find(prefix) {
|
|
|
|
let start_of_user = start_index + prefix.len();
|
|
|
|
let mut end_of_user = result[start_of_user..]
|
|
|
|
.find(|c| c == '/' || c == '\\')
|
|
|
|
.map(|i| start_of_user + i)
|
|
|
|
.unwrap_or(result.len());
|
|
|
|
if end_of_user == start_of_user && start_of_user < result.len() {
|
|
|
|
end_of_user = result.len();
|
|
|
|
}
|
|
|
|
result.replace_range(start_of_user..end_of_user, "<USER>");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-03 01:00:24 -04:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
pub struct Memory {
|
|
|
|
bytes: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Memory {
|
|
|
|
pub const fn from_bytes(bytes: u64) -> Self {
|
|
|
|
Self { bytes }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn from_kb(kb: u64) -> Self {
|
|
|
|
Self { bytes: kb * 1024 }
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn from_mb(mb: u64) -> Self {
|
2025-04-03 01:37:53 -04:00
|
|
|
Self {
|
|
|
|
bytes: mb * 1024 * 1024,
|
|
|
|
}
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn from_gb(gb: u64) -> Self {
|
2025-04-03 01:37:53 -04:00
|
|
|
Self {
|
|
|
|
bytes: gb * 1024 * 1024 * 1024,
|
|
|
|
}
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn as_bytes(&self) -> u64 {
|
|
|
|
self.bytes
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn as_kb(&self) -> u64 {
|
|
|
|
self.bytes / 1024
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn as_mb(&self) -> u64 {
|
|
|
|
self.bytes / (1024 * 1024)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const fn as_gb(&self) -> u64 {
|
|
|
|
self.bytes / (1024 * 1024 * 1024)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn format_human(&self) -> String {
|
|
|
|
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
|
|
|
|
let mut size = self.bytes as f64;
|
|
|
|
let mut unit_index = 0;
|
|
|
|
|
|
|
|
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
|
|
|
size /= 1024.0;
|
|
|
|
unit_index += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
format!("{:.2} {}", size, UNITS[unit_index])
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"{} ({} bytes, {} KB, {} MB, {} GB)",
|
|
|
|
self.format_human(),
|
|
|
|
self.as_bytes(),
|
|
|
|
self.as_kb(),
|
|
|
|
self.as_mb(),
|
|
|
|
self.as_gb()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Memory {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
write!(f, "{}", self.format_human())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
pub enum CPUBrand {
|
|
|
|
Intel,
|
|
|
|
AMD,
|
|
|
|
SnapDragon,
|
|
|
|
Apple,
|
|
|
|
Other(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&str> for CPUBrand {
|
|
|
|
fn from(s: &str) -> Self {
|
|
|
|
let sl = s.to_lowercase();
|
|
|
|
if sl.contains("intel") {
|
|
|
|
Self::Intel
|
|
|
|
} else if sl.contains("amd") {
|
|
|
|
Self::AMD
|
|
|
|
} else if sl.contains("snapdragon") {
|
|
|
|
Self::SnapDragon
|
|
|
|
} else if sl.contains("apple") {
|
|
|
|
Self::Apple
|
|
|
|
} else {
|
|
|
|
Self::Other(s.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for CPUBrand {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
Self::Intel => write!(f, "Intel"),
|
|
|
|
Self::AMD => write!(f, "AMD"),
|
|
|
|
Self::SnapDragon => write!(f, "SnapDragon"),
|
|
|
|
Self::Apple => write!(f, "Apple"),
|
|
|
|
Self::Other(s) => write!(f, "{}", s),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
pub enum CPUArch {
|
|
|
|
X86,
|
|
|
|
X86_64,
|
|
|
|
AArch64,
|
|
|
|
Other(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for CPUArch {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
Ok(match s {
|
|
|
|
"x86" => Self::X86,
|
|
|
|
"x86_64" => Self::X86_64,
|
|
|
|
"arm" => Self::AArch64,
|
|
|
|
"aarch64" => Self::AArch64,
|
|
|
|
_ => Self::Other(s.to_string()),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for CPUArch {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
Self::X86 => write!(f, "x86"),
|
|
|
|
Self::X86_64 => write!(f, "x86_64"),
|
|
|
|
Self::AArch64 => write!(f, "AArch64"),
|
|
|
|
Self::Other(s) => write!(f, "{}", s),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
|
|
pub struct ClockSpeed(pub u32);
|
|
|
|
|
|
|
|
impl ClockSpeed {
|
|
|
|
pub fn as_mhz(&self) -> u32 {
|
|
|
|
self.0
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn as_ghz(&self) -> f32 {
|
|
|
|
self.0 as f32 / 1000.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for ClockSpeed {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
write!(f, "{:.2} GHz ({} MHz)", self.as_ghz(), self.as_mhz())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct CPU {
|
|
|
|
pub brand: CPUBrand,
|
|
|
|
pub arch: CPUArch,
|
|
|
|
pub name: String,
|
|
|
|
pub vendor_id: String,
|
|
|
|
pub physical_cores: Option<u8>,
|
|
|
|
pub logical_cores: Option<u8>,
|
|
|
|
pub max_clock_speed: ClockSpeed,
|
|
|
|
pub current_clock_speed: ClockSpeed,
|
|
|
|
pub l1_cache: Option<Memory>,
|
|
|
|
pub l2_cache: Option<Memory>,
|
|
|
|
pub l3_cache: Option<Memory>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CPU {
|
|
|
|
pub fn current() -> Self {
|
2025-04-03 01:37:53 -04:00
|
|
|
let mut sys = System::new_with_specifics(
|
|
|
|
RefreshKind::default().with_cpu(CpuRefreshKind::everything()),
|
|
|
|
);
|
2025-04-03 01:00:24 -04:00
|
|
|
sys.refresh_cpu_all();
|
|
|
|
|
|
|
|
let cpu_opt = sys.cpus().first();
|
|
|
|
|
2025-04-03 01:37:53 -04:00
|
|
|
let brand = cpu_opt
|
|
|
|
.map(|cpu| cpu.brand().into())
|
2025-04-03 01:00:24 -04:00
|
|
|
.unwrap_or(CPUBrand::Other("unknown".into()));
|
2025-04-03 01:37:53 -04:00
|
|
|
let name = cpu_opt
|
|
|
|
.map(|cpu| cpu.name().to_string())
|
2025-04-03 01:00:24 -04:00
|
|
|
.unwrap_or_else(|| "unknown".into());
|
2025-04-03 01:37:53 -04:00
|
|
|
let vendor_id = cpu_opt
|
|
|
|
.map(|cpu| cpu.vendor_id().to_string())
|
2025-04-03 01:00:24 -04:00
|
|
|
.unwrap_or_else(|| "unknown".into());
|
2025-04-03 01:37:53 -04:00
|
|
|
let max_clock_speed = cpu_opt
|
|
|
|
.map(|cpu| ClockSpeed(cpu.frequency() as u32))
|
2025-04-03 01:00:24 -04:00
|
|
|
.unwrap_or(ClockSpeed(0));
|
|
|
|
let current_clock_speed = max_clock_speed;
|
|
|
|
|
|
|
|
let logical_cores = cpu_opt.map(|_| sys.cpus().len() as u8);
|
2025-04-10 22:57:19 -04:00
|
|
|
let physical_cores = System::physical_core_count().map(|pc| pc as u8);
|
2025-04-03 01:00:24 -04:00
|
|
|
|
|
|
|
let mut l1_cache = None;
|
|
|
|
let mut l2_cache = None;
|
|
|
|
let mut l3_cache = None;
|
2025-04-11 19:54:52 +02:00
|
|
|
|
|
|
|
#[cfg(any(
|
|
|
|
all(target_arch = "x86", not(target_env = "sgx"), target_feature = "sse"),
|
|
|
|
all(target_arch = "x86_64", not(target_env = "sgx"))
|
|
|
|
))]
|
|
|
|
{
|
|
|
|
let cpuid = CpuId::new();
|
|
|
|
if let Some(iter) = cpuid.get_cache_parameters() {
|
|
|
|
for cache in iter {
|
|
|
|
match cache.level() {
|
|
|
|
1 => {
|
|
|
|
let size = cache.physical_line_partitions()
|
|
|
|
* cache.coherency_line_size()
|
|
|
|
* cache.associativity();
|
|
|
|
if size > 0 {
|
|
|
|
l1_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
|
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
}
|
2025-04-11 19:54:52 +02:00
|
|
|
2 => {
|
|
|
|
let size = cache.physical_line_partitions()
|
|
|
|
* cache.coherency_line_size()
|
|
|
|
* cache.associativity();
|
|
|
|
if size > 0 {
|
|
|
|
l2_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
|
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
}
|
2025-04-11 19:54:52 +02:00
|
|
|
3 => {
|
|
|
|
let size = (cache.physical_line_partitions() as u64)
|
|
|
|
* (cache.coherency_line_size() as u64)
|
|
|
|
* (cache.associativity() as u64);
|
|
|
|
if size > 0 {
|
|
|
|
l3_cache = Some(Memory::from_bytes(size));
|
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
}
|
2025-04-11 19:54:52 +02:00
|
|
|
_ => {}
|
2025-04-03 01:37:53 -04:00
|
|
|
}
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Self {
|
|
|
|
brand,
|
2025-04-10 22:57:19 -04:00
|
|
|
arch: env::consts::ARCH
|
2025-04-03 01:37:53 -04:00
|
|
|
.parse()
|
|
|
|
.unwrap_or(CPUArch::Other("unknown".into())),
|
2025-04-03 01:00:24 -04:00
|
|
|
name,
|
|
|
|
vendor_id,
|
|
|
|
physical_cores,
|
|
|
|
logical_cores,
|
|
|
|
max_clock_speed,
|
|
|
|
current_clock_speed,
|
|
|
|
l1_cache,
|
|
|
|
l2_cache,
|
|
|
|
l3_cache,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_intel(&self) -> bool {
|
|
|
|
matches!(self.brand, CPUBrand::Intel)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_amd(&self) -> bool {
|
|
|
|
matches!(self.brand, CPUBrand::AMD)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_arm(&self) -> bool {
|
2025-04-03 01:37:53 -04:00
|
|
|
matches!(self.brand, CPUBrand::Intel)
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_high_clock(&self) -> bool {
|
|
|
|
self.max_clock_speed.0 > 3000
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"CPU Information:\n\
|
|
|
|
- Brand: {}\n\
|
|
|
|
- Architecture: {}\n\
|
|
|
|
- Name: {}\n\
|
|
|
|
- Vendor ID: {}\n\
|
|
|
|
- Cores: {} physical, {} logical\n\
|
|
|
|
- Clock Speed: {} (max), {} (current)\n\
|
|
|
|
- Cache: L1 {}, L2 {}, L3 {}",
|
|
|
|
self.brand,
|
|
|
|
self.arch,
|
|
|
|
self.name,
|
|
|
|
self.vendor_id,
|
2025-04-03 01:37:53 -04:00
|
|
|
self.physical_cores
|
|
|
|
.map(|c| c.to_string())
|
|
|
|
.unwrap_or_else(|| "unknown".into()),
|
|
|
|
self.logical_cores
|
|
|
|
.map(|c| c.to_string())
|
|
|
|
.unwrap_or_else(|| "unknown".into()),
|
2025-04-03 01:00:24 -04:00
|
|
|
self.max_clock_speed,
|
|
|
|
self.current_clock_speed,
|
2025-04-03 01:37:53 -04:00
|
|
|
self.l1_cache
|
|
|
|
.map(|c| c.format_human())
|
|
|
|
.unwrap_or_else(|| "unknown".into()),
|
|
|
|
self.l2_cache
|
|
|
|
.map(|c| c.format_human())
|
|
|
|
.unwrap_or_else(|| "unknown".into()),
|
|
|
|
self.l3_cache
|
|
|
|
.map(|c| c.format_human())
|
|
|
|
.unwrap_or_else(|| "unknown".into()),
|
2025-04-03 01:00:24 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
|
|
pub enum GPUBrand {
|
|
|
|
Nvidia,
|
|
|
|
AMD,
|
|
|
|
Intel,
|
|
|
|
Other(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<&str> for GPUBrand {
|
|
|
|
fn from(s: &str) -> Self {
|
|
|
|
let sl = s.to_lowercase();
|
|
|
|
if sl.contains("nvidia") || sl.contains("geforce") {
|
|
|
|
Self::Nvidia
|
|
|
|
} else if sl.contains("amd") || sl.contains("radeon") {
|
|
|
|
Self::AMD
|
|
|
|
} else if sl.contains("intel") {
|
|
|
|
Self::Intel
|
|
|
|
} else {
|
|
|
|
Self::Other(s.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for GPUBrand {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
Self::Nvidia => write!(f, "NVIDIA"),
|
|
|
|
Self::AMD => write!(f, "AMD"),
|
|
|
|
Self::Intel => write!(f, "Intel"),
|
|
|
|
Self::Other(s) => write!(f, "{}", s),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct GPU {
|
|
|
|
pub brand: GPUBrand,
|
|
|
|
pub name: String,
|
|
|
|
pub device_type: DeviceType,
|
|
|
|
pub vram: Memory,
|
|
|
|
pub driver_version: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl GPU {
|
|
|
|
pub fn current() -> Vec<Self> {
|
|
|
|
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
|
|
|
backends: wgpu::Backends::all(),
|
|
|
|
..Default::default()
|
|
|
|
});
|
|
|
|
|
|
|
|
instance
|
|
|
|
.enumerate_adapters(wgpu::Backends::all())
|
|
|
|
.iter()
|
|
|
|
.map(|adapter| {
|
|
|
|
let info = adapter.get_info();
|
|
|
|
GPU {
|
|
|
|
brand: info.name.as_str().into(),
|
|
|
|
name: info.name.to_string(),
|
|
|
|
device_type: info.device_type,
|
|
|
|
vram: Memory::from_bytes(0),
|
|
|
|
driver_version: Some(info.driver.to_string()),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_integrated(&self) -> bool {
|
|
|
|
matches!(self.brand, GPUBrand::Intel)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn is_dedicated(&self) -> bool {
|
|
|
|
!self.is_integrated()
|
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
|
2025-04-03 01:00:24 -04:00
|
|
|
pub fn is_mobile(&self) -> bool {
|
|
|
|
let lower_name = self.name.to_lowercase();
|
|
|
|
lower_name.contains("adreno")
|
|
|
|
|| lower_name.contains("mali")
|
|
|
|
|| lower_name.contains("apple")
|
|
|
|
|| lower_name.contains("mobile")
|
|
|
|
|| lower_name.contains("snapdragon")
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"GPU Information:\n\
|
|
|
|
- Brand: {}\n\
|
|
|
|
- Name: {}\n\
|
|
|
|
- VRAM: {}\n\
|
|
|
|
- Driver: {}",
|
|
|
|
self.brand,
|
|
|
|
self.name,
|
|
|
|
self.vram.format_human(),
|
|
|
|
self.driver_version.as_deref().unwrap_or("Unknown")
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unique_id(&self) -> String {
|
|
|
|
format!("{}-{:?}", self.name, self.device_type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct SystemMemory {
|
|
|
|
pub total: Memory,
|
|
|
|
pub used: Memory,
|
|
|
|
pub free: Memory,
|
|
|
|
pub available: Memory,
|
|
|
|
pub swap_total: Memory,
|
|
|
|
pub swap_used: Memory,
|
|
|
|
pub swap_free: Memory,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SystemMemory {
|
|
|
|
pub fn current() -> Self {
|
|
|
|
let mut system = System::new();
|
|
|
|
system.refresh_memory();
|
|
|
|
|
|
|
|
Self {
|
|
|
|
total: Memory::from_bytes(system.total_memory()),
|
|
|
|
used: Memory::from_bytes(system.used_memory()),
|
|
|
|
free: Memory::from_bytes(system.free_memory()),
|
|
|
|
available: Memory::from_bytes(system.available_memory()),
|
|
|
|
swap_total: Memory::from_bytes(system.total_swap()),
|
|
|
|
swap_used: Memory::from_bytes(system.used_swap()),
|
|
|
|
swap_free: Memory::from_bytes(system.free_swap()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"Memory Information:\n\
|
|
|
|
- RAM: {} total, {} used, {} free, {} available\n\
|
|
|
|
- Swap: {} total, {} used, {} free",
|
|
|
|
self.total.format_human(),
|
|
|
|
self.used.format_human(),
|
|
|
|
self.free.format_human(),
|
|
|
|
self.available.format_human(),
|
|
|
|
self.swap_total.format_human(),
|
|
|
|
self.swap_used.format_human(),
|
|
|
|
self.swap_free.format_human()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
2025-04-03 18:21:19 -04:00
|
|
|
pub struct EngineInfo {
|
2025-04-03 01:00:24 -04:00
|
|
|
pub timestamp: String,
|
|
|
|
pub pkg_version: String,
|
|
|
|
pub pkg_name: String,
|
|
|
|
pub target_arch: String,
|
|
|
|
pub target_os: String,
|
|
|
|
pub target_env: String,
|
2025-04-03 18:21:19 -04:00
|
|
|
pub rustc_version: String,
|
|
|
|
pub wgpu_version: String,
|
|
|
|
pub winit_version: String,
|
|
|
|
pub commit_hash: String,
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
|
2025-04-03 18:21:19 -04:00
|
|
|
impl EngineInfo {
|
2025-04-03 01:00:24 -04:00
|
|
|
pub fn current() -> Self {
|
|
|
|
Self {
|
|
|
|
timestamp: build_info::BUILT_TIME_UTC.to_string(),
|
|
|
|
pkg_version: build_info::PKG_VERSION.to_string(),
|
|
|
|
pkg_name: build_info::PKG_NAME.to_string(),
|
|
|
|
target_arch: build_info::CFG_TARGET_ARCH.to_string(),
|
|
|
|
target_os: build_info::CFG_OS.to_string(),
|
|
|
|
target_env: build_info::CFG_ENV.to_string(),
|
2025-04-03 18:21:19 -04:00
|
|
|
rustc_version: build_info::RUSTC_VERSION.to_string(),
|
|
|
|
wgpu_version: build_info::WGPU_VERSION.to_string(),
|
|
|
|
winit_version: build_info::WGPU_VERSION.to_string(),
|
2025-04-10 14:26:52 -04:00
|
|
|
commit_hash: build_info::GIT_COMMIT_HASH
|
|
|
|
.unwrap_or(&format!("UNKNOWN-{:?}", std::time::SystemTime::now()))
|
|
|
|
.to_string(),
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"Build Information:\n\
|
|
|
|
- Timestamp: {}\n\
|
|
|
|
- Package: {} v{}\n\
|
|
|
|
- Target: {} {} {}\n\
|
2025-04-03 18:21:19 -04:00
|
|
|
- Rustc version: {}\n\
|
|
|
|
- Wgpu version: {}\n\
|
|
|
|
- Winit version: {}\n\
|
|
|
|
- commit_hash: {}\n\
|
2025-04-03 01:00:24 -04:00
|
|
|
",
|
|
|
|
self.timestamp,
|
|
|
|
self.pkg_name,
|
|
|
|
self.pkg_version,
|
|
|
|
self.target_arch,
|
|
|
|
self.target_os,
|
2025-04-03 18:21:19 -04:00
|
|
|
self.target_env,
|
|
|
|
self.rustc_version,
|
|
|
|
self.wgpu_version,
|
|
|
|
self.winit_version,
|
|
|
|
self.commit_hash
|
2025-04-03 01:00:24 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct SystemMetadata {
|
|
|
|
pub cpu: CPU,
|
|
|
|
pub memory: SystemMemory,
|
|
|
|
pub gpus: Vec<GPU>,
|
2025-04-03 18:21:19 -04:00
|
|
|
pub compile_info: EngineInfo,
|
2025-04-10 14:26:52 -04:00
|
|
|
pub os_info: OSInfo,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
pub struct OSInfo {
|
|
|
|
pub name: String,
|
|
|
|
pub version: Option<String>,
|
|
|
|
pub kernel_version: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl OSInfo {
|
|
|
|
pub fn current() -> Self {
|
|
|
|
let mut system = System::new();
|
|
|
|
system.refresh_all();
|
|
|
|
Self {
|
2025-04-10 22:57:19 -04:00
|
|
|
name: System::name().unwrap_or_else(|| build_info::TARGET.to_string()),
|
|
|
|
version: System::os_version(),
|
|
|
|
kernel_version: System::kernel_version(),
|
2025-04-10 14:26:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_info(&self) -> String {
|
|
|
|
format!(
|
|
|
|
"Operating System Information:\n\
|
|
|
|
- Name: {}\n\
|
|
|
|
- Version: {}\n\
|
|
|
|
- Kernel Version: {}",
|
|
|
|
self.name,
|
|
|
|
self.version.as_deref().unwrap_or("Unknown"),
|
|
|
|
self.kernel_version.as_deref().unwrap_or("Unknown")
|
|
|
|
)
|
|
|
|
}
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SystemMetadata {
|
|
|
|
pub fn current() -> Self {
|
|
|
|
Self {
|
|
|
|
cpu: CPU::current(),
|
|
|
|
memory: SystemMemory::current(),
|
|
|
|
gpus: GPU::current(),
|
2025-04-03 18:21:19 -04:00
|
|
|
compile_info: EngineInfo::current(),
|
2025-04-10 14:26:52 -04:00
|
|
|
os_info: OSInfo::current(),
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
|
2025-04-03 01:00:24 -04:00
|
|
|
pub fn main_gpu(&self) -> Option<&GPU> {
|
|
|
|
self.gpus
|
|
|
|
.iter()
|
|
|
|
.find(|g| g.is_dedicated())
|
|
|
|
.or_else(|| self.gpus.iter().find(|g| g.is_integrated()))
|
|
|
|
.or_else(|| self.gpus.first())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn verbose_summary(&self) -> String {
|
|
|
|
let main_gpu = self.main_gpu();
|
|
|
|
let main_gpu_info = main_gpu
|
|
|
|
.map(|gpu| format!("Main GPU:\n{}", gpu.verbose_info()))
|
|
|
|
.unwrap_or_else(|| "No main GPU detected".to_string());
|
|
|
|
|
|
|
|
let mut seen_gpu_ids = HashSet::new();
|
|
|
|
let mut other_gpus_info = Vec::new();
|
|
|
|
|
|
|
|
if let Some(gpu) = main_gpu {
|
|
|
|
seen_gpu_ids.insert(gpu.unique_id());
|
|
|
|
}
|
|
|
|
|
|
|
|
for gpu in &self.gpus {
|
|
|
|
let gpu_id = gpu.unique_id();
|
|
|
|
if !seen_gpu_ids.contains(&gpu_id) {
|
|
|
|
other_gpus_info.push(format!("[{:?}] {}", gpu.device_type, gpu.verbose_info()));
|
|
|
|
seen_gpu_ids.insert(gpu_id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let other_gpu_list = if other_gpus_info.is_empty() {
|
|
|
|
String::new()
|
|
|
|
} else {
|
|
|
|
format!("Other GPUs:\n{}", other_gpus_info.join("\n\n"))
|
|
|
|
};
|
|
|
|
|
|
|
|
format!(
|
2025-04-10 14:26:52 -04:00
|
|
|
"System Information:\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}",
|
|
|
|
self.os_info.verbose_info(),
|
2025-04-03 01:00:24 -04:00
|
|
|
self.cpu.verbose_info(),
|
|
|
|
main_gpu_info,
|
|
|
|
other_gpu_list,
|
|
|
|
self.memory.verbose_info(),
|
|
|
|
self.compile_info.verbose_info()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_memory_conversions() {
|
|
|
|
let mem = Memory::from_gb(8);
|
|
|
|
assert_eq!(mem.as_bytes(), 8 * 1024 * 1024 * 1024);
|
|
|
|
assert_eq!(mem.as_mb(), 8 * 1024);
|
|
|
|
assert_eq!(mem.as_kb(), 8 * 1024 * 1024);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_system_metadata() {
|
|
|
|
let metadata = SystemMetadata::current();
|
|
|
|
assert!(!metadata.cpu.name.is_empty());
|
|
|
|
assert!(metadata.memory.total.as_bytes() > 0);
|
|
|
|
assert!(!metadata.compile_info.pkg_version.is_empty());
|
2025-04-10 14:26:52 -04:00
|
|
|
assert!(!metadata.os_info.name.is_empty());
|
2025-04-03 01:00:24 -04:00
|
|
|
}
|
2025-04-03 01:37:53 -04:00
|
|
|
}
|