From dd4ee230b14e08ce700342bd25f175636171adeb Mon Sep 17 00:00:00 2001 From: Manupa Wickramasinghe <73810867+manupawickramasinghe@users.noreply.github.com> Date: Thu, 27 Aug 2026 03:29:42 +0530 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Decode=20the=20whole=20SMBIO?= =?UTF-8?q?S=20memory-type=20table,=20not=20three=20values=20of=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Summary reported the literal string "DRAM SDRAM" for most current laptops. Two bugs compounding. `Win32_PhysicalMemory.SMBIOSMemoryType` was decoded for DDR3, DDR4 and DDR5 only, with everything else flattened to "DRAM" — and every soldered-memory machine reports an LPDDR code, so that fallback covered most portable hardware sold today. The Summary then appended " SDRAM" to whatever it was given, turning the fallback into nonsense. `smbios_memory_type` now decodes the full DMTF table, codes 0x01 through 0x24, cross-checked against dmidecode's `dmi_memory_device_type`. That adds LPDDR through LPDDR5, HBM/HBM2/HBM3, and the older SDRAM/RDRAM/DDR/DDR2 codes. A code the table does not know reports as `Unknown (type 0x25)` rather than being flattened into a wrong name. DMTF assigns new codes as memory generations ship, and the raw number is what lets someone look one up. The suffix is now applied by `memory_type_label`, which adds " SDRAM" only to the DDR and LPDDR families — "HBM3" and "Unknown" are left alone. Six tests cover the mapping; they take the raw code rather than reading WMI, so they run on every CI leg. Verified on a DDR5 desktop, which still reports DDR5 through `sensorview info`. `memory_type_label` is marked `#[allow(dead_code)]` because only the GUI renders it — without that the headless legs fail CI's `-D warnings`. Not fixed here, but adjacent and worth a separate look: the same Memory panel infers channel count from the module count, so four DIMMs on a dual-channel board report "Quad-Channel". Co-Authored-By: Claude Opus 5 --- app/src/sysinfo.rs | 1688 ++++++++++++++++++---------------- app/src/ui/summary_window.rs | 790 ++++++++-------- 2 files changed, 1301 insertions(+), 1177 deletions(-) diff --git a/app/src/sysinfo.rs b/app/src/sysinfo.rs index 09c86e85..4363a696 100644 --- a/app/src/sysinfo.rs +++ b/app/src/sysinfo.rs @@ -1,782 +1,906 @@ -//! Static system information for the Main window tree and the System Summary. -//! -//! Queried once at startup on a background thread (WMI/COM on Windows; minimal -//! fallbacks elsewhere). Anything a source can't provide stays `None` and the -//! UI renders "—" — honest placeholders until the native engine (SMBus SPD, -//! CPUID, NVML/ADL) fills them in. - -use std::sync::{Arc, RwLock}; - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct CpuInfo { - pub name: String, - pub cores: Option, - pub threads: Option, - pub base_clock_mhz: Option, - pub max_clock_mhz: Option, - pub l2_kb: Option, - pub l3_kb: Option, - pub socket: Option, - /// CPUID(1).EAX signature, HWiNFO-style hex (e.g. "00A60F12"). - pub cpuid: String, - /// Best-effort microarchitecture codename (e.g. "Raphael (Zen 4)"). - pub codename: String, - pub vendor: String, - /// ISA feature names detected at runtime (for the Summary features grid). - pub features: Vec<(&'static str, bool)>, -} - -/// Raw CPUID(1).EAX signature + vendor + codename, computed on x86_64. -fn cpuid_info() -> (String, String, String) { - #[cfg(target_arch = "x86_64")] - { - use core::arch::x86_64::__cpuid; - // __cpuid is safe on x86_64 (CPUID is always available). - let vendor_leaf = __cpuid(0); - let mut vbytes = Vec::new(); - vbytes.extend_from_slice(&vendor_leaf.ebx.to_le_bytes()); - vbytes.extend_from_slice(&vendor_leaf.edx.to_le_bytes()); - vbytes.extend_from_slice(&vendor_leaf.ecx.to_le_bytes()); - let vendor = String::from_utf8_lossy(&vbytes).to_string(); - - let leaf1 = __cpuid(1); - let eax = leaf1.eax; - let base_family = (eax >> 8) & 0xf; - let ext_family = (eax >> 20) & 0xff; - let family = if base_family == 0xf { base_family + ext_family } else { base_family }; - let base_model = (eax >> 4) & 0xf; - let ext_model = (eax >> 16) & 0xf; - let model = (ext_model << 4) | base_model; - - let codename = codename_for(&vendor, family, model); - (format!("{eax:08X}"), vendor, codename) - } - // Apple Silicon has no CPUID. The nearest equivalents are the board id - // (`hw.model`, e.g. "Mac17,3") and the SoC name from the brand string, so - // report those rather than leaving the Summary window blank. - #[cfg(all(target_arch = "aarch64", target_os = "macos"))] - { - let vendor = if sysctl_string("machdep.cpu.brand_string") - .is_some_and(|b| b.starts_with("Apple")) - { - "Apple".to_string() - } else { - String::new() - }; - (String::new(), vendor, sysctl_string("hw.model").unwrap_or_default()) - } - #[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "macos"))))] - { - (String::new(), String::new(), String::new()) - } -} - -/// Coarse codename map for recent AMD/Intel desktop parts (best effort). -#[allow(dead_code)] -fn codename_for(vendor: &str, family: u32, model: u32) -> String { - if vendor.contains("AuthenticAMD") { - match (family, model) { - (0x19, 0x60..=0x6f) => "Raphael (Zen 4)", - (0x19, 0x70..=0x7f) => "Phoenix (Zen 4)", - (0x19, 0x40..=0x4f) => "Rembrandt (Zen 3+)", - (0x19, 0x20..=0x2f) => "Vermeer (Zen 3)", - (0x19, 0x50..=0x5f) => "Cezanne (Zen 3)", - (0x1a, _) => "Granite Ridge (Zen 5)", - (0x17, _) => "Matisse/Renoir (Zen 2)", - _ => "", - } - .to_string() - } else if vendor.contains("GenuineIntel") { - match family { - 0x6 => "Intel Core", - _ => "", - } - .to_string() - } else { - String::new() - } -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct BoardInfo { - pub product: String, - pub manufacturer: String, - pub bios_version: String, - pub bios_date: String, -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct MemoryModule { - pub bank: String, - pub manufacturer: String, - pub part_number: String, - pub capacity_gb: f64, - pub speed_mts: Option, - pub configured_speed_mts: Option, - pub voltage_mv: Option, - pub memory_type: String, -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct GpuInfo { - pub name: String, - /// WMI AdapterRAM (u32, capped at 4 GB) — kept for the native engine to - /// replace with NVML/ADL truth; not displayed while unreliable. - #[allow(dead_code)] - pub vram_gb: Option, - pub driver_version: String, -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct DriveInfo { - pub model: String, - pub interface: String, - pub size_gb: Option, -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct OsInfo { - pub caption: String, - pub build: String, - pub arch: String, - pub uefi_boot: Option, - pub secure_boot: Option, -} - -#[derive(Debug, Default, Clone, serde::Serialize)] -pub struct SystemInfo { - pub computer_name: String, - pub user_name: String, - pub cpu: CpuInfo, - pub board: BoardInfo, - pub memory_modules: Vec, - pub total_memory_gb: Option, - pub gpus: Vec, - pub drives: Vec, - pub os: OsInfo, -} - -/// Shared handle: `None` until the background query completes. -pub type SystemInfoHandle = Arc>>; - -/// Whether this process is running elevated (`Some(true/false)` on Windows, -/// `None` elsewhere). Reliable and independent of the sidecar — the sidecar is -/// our child, so it inherits our elevation. -// Surfaced as the GUI's "Running as Administrator" badge. -#[allow(dead_code)] -pub fn is_elevated() -> Option { - #[cfg(windows)] - { - #[repr(C)] - struct TokenElevation { - token_is_elevated: u32, - } - const TOKEN_QUERY: u32 = 0x0008; - const TOKEN_ELEVATION_CLASS: i32 = 20; // TokenElevation - - #[link(name = "advapi32")] - extern "system" { - fn OpenProcessToken(process: isize, desired: u32, handle: *mut isize) -> i32; - fn GetTokenInformation( - token: isize, - class: i32, - info: *mut core::ffi::c_void, - len: u32, - ret_len: *mut u32, - ) -> i32; - } - extern "system" { - fn GetCurrentProcess() -> isize; - fn CloseHandle(h: isize) -> i32; - } - - unsafe { - let mut token: isize = 0; - if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 { - return None; - } - let mut elevation = TokenElevation { token_is_elevated: 0 }; - let mut ret_len = 0u32; - let ok = GetTokenInformation( - token, - TOKEN_ELEVATION_CLASS, - &mut elevation as *mut _ as *mut core::ffi::c_void, - core::mem::size_of::() as u32, - &mut ret_len, - ); - CloseHandle(token); - if ok == 0 { - None - } else { - Some(elevation.token_is_elevated != 0) - } - } - } - // Reported for the status badge only. Nothing on macOS *needs* root: the - // IOKit backend reads every sensor unprivileged, so no feature is gated on - // this (see ui/settings_dialog.rs). - #[cfg(target_os = "macos")] - { - Some(unsafe { libc::geteuid() } == 0) - } - #[cfg(not(any(windows, target_os = "macos")))] - { - None - } -} - -// ---- sysctl helpers (macOS) --------------------------------------------- - -/// Read a string-valued sysctl by name. `None` if the key doesn't exist — -/// keys come and go between macOS releases, so every caller must tolerate it. -#[cfg(target_os = "macos")] -pub(crate) fn sysctl_string(name: &str) -> Option { - let cname = std::ffi::CString::new(name).ok()?; - let mut len = 0usize; - // First call with a null buffer asks for the required size. - if unsafe { - libc::sysctlbyname(cname.as_ptr(), std::ptr::null_mut(), &mut len, std::ptr::null_mut(), 0) - } != 0 - || len == 0 - { - return None; - } - let mut buf = vec![0u8; len]; - if unsafe { - libc::sysctlbyname( - cname.as_ptr(), - buf.as_mut_ptr().cast(), - &mut len, - std::ptr::null_mut(), - 0, - ) - } != 0 - { - return None; - } - buf.truncate(len); - // sysctl strings are NUL-terminated; drop the terminator and anything after. - if let Some(nul) = buf.iter().position(|&b| b == 0) { - buf.truncate(nul); - } - let s = String::from_utf8_lossy(&buf).trim().to_string(); - (!s.is_empty()).then_some(s) -} - -/// Read an integer-valued sysctl. Handles both the 4-byte and 8-byte widths the -/// kernel uses (`hw.ncpu` is 32-bit, `hw.memsize` is 64-bit). -#[cfg(target_os = "macos")] -pub(crate) fn sysctl_u64(name: &str) -> Option { - let cname = std::ffi::CString::new(name).ok()?; - let mut value = 0u64; - let mut len = std::mem::size_of::(); - if unsafe { - libc::sysctlbyname( - cname.as_ptr(), - (&mut value as *mut u64).cast(), - &mut len, - std::ptr::null_mut(), - 0, - ) - } != 0 - { - return None; - } - match len { - 8 => Some(value), - // The kernel wrote only the low 4 bytes; the upper half is our zeroed - // initialiser, so mask rather than trusting the whole u64. - 4 => Some(value & 0xffff_ffff), - _ => None, - } -} - -/// Synchronous `query()` for tests and diagnostics — `spawn_query` returns a -/// handle that only fills in later, which is awkward to assert against. -/// -/// Only the macOS system-profile test consumes it today; `allow(dead_code)` -/// keeps `clippy -D warnings` green on platforms that have no caller yet. -#[cfg(test)] -#[allow(dead_code)] -pub fn query_for_test() -> SystemInfo { - query() -} - -/// Kick off the (slow) WMI enumeration without blocking the UI. -pub fn spawn_query() -> SystemInfoHandle { - let handle: SystemInfoHandle = Arc::new(RwLock::new(None)); - let sink = handle.clone(); - std::thread::spawn(move || { - let info = query(); - if let Ok(mut slot) = sink.write() { - *slot = Some(info); - } - }); - handle -} - -fn cpu_features() -> Vec<(&'static str, bool)> { - #[cfg(target_arch = "x86_64")] - { - vec![ - ("MMX", is_x86_feature_detected!("mmx")), - ("SSE", is_x86_feature_detected!("sse")), - ("SSE2", is_x86_feature_detected!("sse2")), - ("SSE3", is_x86_feature_detected!("sse3")), - ("SSSE3", is_x86_feature_detected!("ssse3")), - ("SSE4.1", is_x86_feature_detected!("sse4.1")), - ("SSE4.2", is_x86_feature_detected!("sse4.2")), - ("SSE4A", is_x86_feature_detected!("sse4a")), - ("AVX", is_x86_feature_detected!("avx")), - ("AVX2", is_x86_feature_detected!("avx2")), - ("AVX-512F", is_x86_feature_detected!("avx512f")), - ("FMA", is_x86_feature_detected!("fma")), - ("BMI1", is_x86_feature_detected!("bmi1")), - ("BMI2", is_x86_feature_detected!("bmi2")), - ("AES-NI", is_x86_feature_detected!("aes")), - ("SHA", is_x86_feature_detected!("sha")), - ("RDRAND", is_x86_feature_detected!("rdrand")), - ("RDSEED", is_x86_feature_detected!("rdseed")), - ("POPCNT", is_x86_feature_detected!("popcnt")), - ("F16C", is_x86_feature_detected!("f16c")), - ] - } - // Apple Silicon: the kernel publishes ~80 `hw.optional.arm.FEAT_*` flags. - // Curated rather than enumerated so the grid stays readable and the labels - // stay `&'static str` — the full list is mostly MTE/SME sub-variants. - #[cfg(all(target_arch = "aarch64", target_os = "macos"))] - { - const FEATURES: &[(&str, &str)] = &[ - ("NEON", "hw.optional.arm.AdvSIMD"), - ("FP16", "hw.optional.arm.FEAT_FP16"), - ("BF16", "hw.optional.arm.FEAT_BF16"), - ("I8MM", "hw.optional.arm.FEAT_I8MM"), - ("DotProd", "hw.optional.arm.FEAT_DotProd"), - ("FHM", "hw.optional.arm.FEAT_FHM"), - ("CRC32", "hw.optional.arm.FEAT_CRC32"), - ("AES", "hw.optional.arm.FEAT_AES"), - ("PMULL", "hw.optional.arm.FEAT_PMULL"), - ("SHA1", "hw.optional.arm.FEAT_SHA1"), - ("SHA256", "hw.optional.arm.FEAT_SHA256"), - ("SHA3", "hw.optional.arm.FEAT_SHA3"), - ("SHA512", "hw.optional.arm.FEAT_SHA512"), - ("LSE", "hw.optional.arm.FEAT_LSE"), - ("LSE2", "hw.optional.arm.FEAT_LSE2"), - ("RDM", "hw.optional.arm.FEAT_RDM"), - ("JSCVT", "hw.optional.arm.FEAT_JSCVT"), - ("FCMA", "hw.optional.arm.FEAT_FCMA"), - ("LRCPC", "hw.optional.arm.FEAT_LRCPC"), - ("PAuth", "hw.optional.arm.FEAT_PAuth"), - ("BTI", "hw.optional.arm.FEAT_BTI"), - ("MTE", "hw.optional.arm.FEAT_MTE"), - ("DIT", "hw.optional.arm.FEAT_DIT"), - ("ECV", "hw.optional.arm.FEAT_ECV"), - ("SME", "hw.optional.arm.FEAT_SME"), - ("SME2", "hw.optional.arm.FEAT_SME2"), - ("SSBS", "hw.optional.arm.FEAT_SSBS"), - ("SPECRES", "hw.optional.arm.FEAT_SPECRES"), - ]; - FEATURES - .iter() - .map(|(label, key)| (*label, sysctl_u64(key).unwrap_or(0) != 0)) - .collect() - } - #[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "macos"))))] - { - Vec::new() - } -} - -#[cfg(windows)] -fn query() -> SystemInfo { - use std::collections::HashMap; - use wmi::{Variant, WMIConnection}; - - let mut info = SystemInfo { - computer_name: std::env::var("COMPUTERNAME").unwrap_or_default(), - user_name: std::env::var("USERNAME").unwrap_or_default(), - ..Default::default() - }; - info.cpu.features = cpu_features(); - let (cpuid, vendor, codename) = cpuid_info(); - info.cpu.cpuid = cpuid; - info.cpu.vendor = vendor; - info.cpu.codename = codename; - - let Ok(wmi) = WMIConnection::new() else { return info }; - - type Row = HashMap; - - let s = |v: Option<&Variant>| -> String { - match v { - Some(Variant::String(x)) => x.trim().to_string(), - _ => String::new(), - } - }; - let u = |v: Option<&Variant>| -> Option { - match v { - Some(Variant::UI4(x)) => Some(*x), - Some(Variant::I4(x)) => u32::try_from(*x).ok(), - Some(Variant::UI2(x)) => Some(*x as u32), - Some(Variant::String(x)) => x.parse().ok(), - _ => None, - } - }; - let u64v = |v: Option<&Variant>| -> Option { - match v { - Some(Variant::UI8(x)) => Some(*x), - Some(Variant::I8(x)) => u64::try_from(*x).ok(), - Some(Variant::UI4(x)) => Some(*x as u64), - Some(Variant::String(x)) => x.parse().ok(), - _ => None, - } - }; - - if let Ok(rows) = wmi.raw_query::( - "SELECT Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, L2CacheSize, L3CacheSize, SocketDesignation FROM Win32_Processor", - ) { - if let Some(r) = rows.first() { - info.cpu.name = s(r.get("Name")); - info.cpu.cores = u(r.get("NumberOfCores")); - info.cpu.threads = u(r.get("NumberOfLogicalProcessors")); - info.cpu.max_clock_mhz = u(r.get("MaxClockSpeed")); - info.cpu.base_clock_mhz = u(r.get("MaxClockSpeed")); - info.cpu.l2_kb = u(r.get("L2CacheSize")); - info.cpu.l3_kb = u(r.get("L3CacheSize")); - info.cpu.socket = Some(s(r.get("SocketDesignation"))).filter(|x| !x.is_empty()); - } - } - - if let Ok(rows) = wmi.raw_query::("SELECT Product, Manufacturer FROM Win32_BaseBoard") { - if let Some(r) = rows.first() { - info.board.product = s(r.get("Product")); - info.board.manufacturer = s(r.get("Manufacturer")); - } - } - if let Ok(rows) = wmi.raw_query::("SELECT SMBIOSBIOSVersion, ReleaseDate FROM Win32_BIOS") { - if let Some(r) = rows.first() { - info.board.bios_version = s(r.get("SMBIOSBIOSVersion")); - let date = s(r.get("ReleaseDate")); - // WMI CIM_DATETIME: yyyymmddHHMMSS… → mm/dd/yyyy like HWiNFO shows. - if date.len() >= 8 { - info.board.bios_date = format!("{}/{}/{}", &date[4..6], &date[6..8], &date[0..4]); - } - } - } - - if let Ok(rows) = wmi.raw_query::( - "SELECT BankLabel, DeviceLocator, Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, ConfiguredVoltage, SMBIOSMemoryType FROM Win32_PhysicalMemory", - ) { - let mut total = 0.0; - for r in &rows { - let capacity_gb = u64v(r.get("Capacity")).map(|b| b as f64 / (1u64 << 30) as f64).unwrap_or(0.0); - total += capacity_gb; - let mem_type = match u(r.get("SMBIOSMemoryType")) { - Some(26) => "DDR4", - Some(34) => "DDR5", - Some(24) => "DDR3", - _ => "DRAM", - }; - info.memory_modules.push(MemoryModule { - bank: { - let bank = s(r.get("BankLabel")); - let loc = s(r.get("DeviceLocator")); - if bank.is_empty() { loc } else { format!("{bank}/{loc}") } - }, - manufacturer: s(r.get("Manufacturer")), - part_number: s(r.get("PartNumber")), - capacity_gb, - speed_mts: u(r.get("Speed")), - configured_speed_mts: u(r.get("ConfiguredClockSpeed")), - voltage_mv: u(r.get("ConfiguredVoltage")), - memory_type: mem_type.to_string(), - }); - } - if total > 0.0 { - info.total_memory_gb = Some(total); - } - } - - if let Ok(rows) = wmi.raw_query::("SELECT Name, AdapterRAM, DriverVersion FROM Win32_VideoController") { - for r in &rows { - info.gpus.push(GpuInfo { - name: s(r.get("Name")), - vram_gb: u64v(r.get("AdapterRAM")).map(|b| b as f64 / (1u64 << 30) as f64), - driver_version: s(r.get("DriverVersion")), - }); - } - } - - if let Ok(rows) = wmi.raw_query::("SELECT Model, InterfaceType, Size FROM Win32_DiskDrive") { - for r in &rows { - info.drives.push(DriveInfo { - model: s(r.get("Model")), - interface: s(r.get("InterfaceType")), - size_gb: u64v(r.get("Size")).map(|b| b as f64 / 1_000_000_000.0), - }); - } - } - - if let Ok(rows) = wmi.raw_query::("SELECT Caption, BuildNumber, OSArchitecture FROM Win32_OperatingSystem") { - if let Some(r) = rows.first() { - info.os.caption = s(r.get("Caption")); - info.os.build = s(r.get("BuildNumber")); - info.os.arch = s(r.get("OSArchitecture")); - } - } - - // Secure Boot / UEFI: registry flag (no clean WMI class for it). - info.os.secure_boot = read_secure_boot(); - info.os.uefi_boot = info.os.secure_boot.map(|_| true); - - info -} - -#[cfg(windows)] -fn read_secure_boot() -> Option { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x0800_0000; - let out = std::process::Command::new("reg") - .args([ - "query", - r"HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\State", - "/v", - "UEFISecureBootEnabled", - ]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .ok()?; - let text = String::from_utf8_lossy(&out.stdout); - if text.contains("0x1") { - Some(true) - } else if text.contains("0x0") { - Some(false) - } else { - None - } -} - -#[cfg(target_os = "macos")] -fn query() -> SystemInfo { - let (cpuid, vendor, codename) = cpuid_info(); - - // Apple Silicon is heterogeneous: perflevel0 is the fast cluster (named - // "Performance" through M4, "Super" on M5 — read the name, don't hardcode - // it) and perflevel1 the efficiency cluster. Sum both for the core count, - // and report the layout in `socket` since there is no socket to speak of. - let mut cluster_desc = Vec::new(); - let mut physical = 0u32; - for level in 0..4 { - let Some(count) = sysctl_u64(&format!("hw.perflevel{level}.physicalcpu")) else { - break; - }; - physical += count as u32; - let name = sysctl_string(&format!("hw.perflevel{level}.name")) - .unwrap_or_else(|| format!("level{level}")); - cluster_desc.push(format!("{count} {name}")); - } - // Fall back to the flat count on any Mac that doesn't publish perflevels. - let cores = if physical > 0 { Some(physical) } else { sysctl_u64("hw.physicalcpu").map(|v| v as u32) }; - - let total_memory_gb = sysctl_u64("hw.memsize").map(|b| b as f64 / (1024.0 * 1024.0 * 1024.0)); - - // The SoC is soldered, so there are no per-DIMM SPD entries to enumerate; - // present the unified memory as a single honest module. - let memory_modules = total_memory_gb - .map(|gb| { - vec![MemoryModule { - bank: "Unified Memory".into(), - manufacturer: "Apple".into(), - capacity_gb: gb, - memory_type: "LPDDR (on-package)".into(), - ..Default::default() - }] - }) - .unwrap_or_default(); - - // The integrated GPU shares the SoC; name it after the chip and its core - // count rather than inventing a discrete-adapter identity. - let chip = sysctl_string("machdep.cpu.brand_string").unwrap_or_default(); - let (gpu_cores, metal_driver) = crate::source::macos::sysprofile::gpu_identity(); - let gpus = if chip.is_empty() { - Vec::new() - } else { - let name = match gpu_cores { - Some(cores) => format!("{chip} GPU ({cores} cores)"), - None => format!("{chip} GPU"), - }; - vec![GpuInfo { - name, - // Unified memory — there is no separate VRAM pool to report, and - // quoting total system RAM here would be misleading. - vram_gb: None, - driver_version: metal_driver.unwrap_or_default(), - }] - }; - - // Internal SSD(s), so the Summary's Drives panel isn't blank. - const GB: f64 = 1024.0 * 1024.0 * 1024.0; - let drives = crate::source::macos::sysprofile::drives() - .into_iter() - .map(|(model, bytes)| DriveInfo { - model, - interface: "NVMe".into(), - size_gb: bytes.map(|b| b as f64 / GB), - }) - .collect(); - - // Performance-cluster DVFS states, for the Base/Max Clock rows. - let p_states = - crate::source::macos::dvfs::frequencies_mhz(crate::source::macos::dvfs::Block::Pcpu); - - let os_version = sysctl_string("kern.osproductversion").unwrap_or_default(); - - SystemInfo { - computer_name: sysctl_string("kern.hostname").unwrap_or_default(), - user_name: std::env::var("USER").unwrap_or_default(), - cpu: CpuInfo { - name: chip, - cores, - // Apple Silicon has no SMT: one thread per physical core. - threads: sysctl_u64("hw.logicalcpu").map(|v| v as u32), - // There is no fixed "base clock" on Apple Silicon; the closest - // honest equivalents are the bottom and top of the performance - // cluster's DVFS table. - base_clock_mhz: p_states.first().map(|mhz| *mhz as u32), - max_clock_mhz: p_states.last().map(|mhz| *mhz as u32), - // hw.l2cachesize reports the *efficiency* cluster (6 MB here). - // The headline figure is the performance cluster's L2 - // (hw.perflevel0.l2cachesize, 16 MB), so prefer that. - l2_kb: sysctl_u64("hw.perflevel0.l2cachesize") - .or_else(|| sysctl_u64("hw.l2cachesize")) - .map(|b| (b / 1024) as u32), - // Apple Silicon has no per-core L3; the system-level cache is not - // published anywhere readable, so this stays honestly blank. - l3_kb: None, - socket: (!cluster_desc.is_empty()).then(|| cluster_desc.join(" + ")), - features: cpu_features(), - cpuid, - vendor, - codename, - }, - board: BoardInfo { - // Prefer the marketing name ("MacBook Air (13-inch, M5)") over the - // bare board id ("Mac17,3"), which is already shown as the codename. - product: crate::source::macos::sysprofile::product_name() - .or_else(|| sysctl_string("hw.model")) - .unwrap_or_default(), - manufacturer: "Apple Inc.".into(), - // Apple Silicon boots via iBoot, so the closest thing to a BIOS - // version is the boot firmware revision. - bios_version: crate::source::macos::sysprofile::firmware_version().unwrap_or_default(), - // No firmware build date is published anywhere in IOKit; leave it - // blank rather than guessing from the version string. - bios_date: String::new(), - }, - memory_modules, - total_memory_gb, - gpus, - drives, - os: OsInfo { - caption: if os_version.is_empty() { - "macOS".into() - } else { - format!("macOS {os_version}") - }, - build: sysctl_string("kern.osversion").unwrap_or_default(), - arch: std::env::consts::ARCH.to_string(), - // Apple Silicon boots via iBoot, not UEFI, and Secure Boot state - // lives in a different subsystem — leave both indeterminate rather - // than asserting something false. - uefi_boot: None, - secure_boot: None, - }, - } -} - -#[cfg(target_os = "linux")] -fn query() -> SystemInfo { - use std::fs; - let (cpuid, vendor, codename) = cpuid_info(); - let mut info = SystemInfo { - computer_name: std::env::var("HOSTNAME").unwrap_or_default(), - user_name: std::env::var("USER").unwrap_or_default(), - cpu: CpuInfo { features: cpu_features(), cpuid, vendor, codename, ..Default::default() }, - ..Default::default() - }; - - // Read CPU Model Name and Cores from /proc/cpuinfo - if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") { - let mut model_name = String::new(); - let mut logical_count = 0u32; - for line in cpuinfo.lines() { - if line.starts_with("model name") { - if let Some(val) = line.split(':').nth(1) { - if model_name.is_empty() { - model_name = val.trim().to_string(); - } - } - } - if line.starts_with("processor") { - logical_count += 1; - } - } - if !model_name.is_empty() { - info.cpu.name = model_name; - } - if logical_count > 0 { - info.cpu.threads = Some(logical_count); - info.cpu.cores = Some(logical_count); // best-effort fallback - } - } - - // Read Motherboard / DMI Info from /sys/class/dmi/id - if let Ok(product) = fs::read_to_string("/sys/class/dmi/id/board_name") { - info.board.product = product.trim().to_string(); - } - if let Ok(vendor) = fs::read_to_string("/sys/class/dmi/id/board_vendor") { - info.board.manufacturer = vendor.trim().to_string(); - } - if let Ok(version) = fs::read_to_string("/sys/class/dmi/id/bios_version") { - info.board.bios_version = version.trim().to_string(); - } - if let Ok(date) = fs::read_to_string("/sys/class/dmi/id/bios_date") { - info.board.bios_date = date.trim().to_string(); - } - - // Read RAM Total from /proc/meminfo - if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") { - for line in meminfo.lines() { - if line.starts_with("MemTotal:") { - if let Some(kb_str) = line.split_whitespace().nth(1) { - if let Ok(kb) = kb_str.parse::() { - info.total_memory_gb = Some(kb / (1024.0 * 1024.0)); - } - } - } - } - } - - // Read OS info from /etc/os-release - if let Ok(os_release) = fs::read_to_string("/etc/os-release") { - for line in os_release.lines() { - if line.starts_with("PRETTY_NAME=") { - let name = line.trim_start_matches("PRETTY_NAME=").trim_matches('"'); - info.os.caption = name.to_string(); - } - } - } - - info -} - -#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] -fn query() -> SystemInfo { - let (cpuid, vendor, codename) = cpuid_info(); - SystemInfo { - computer_name: std::env::var("HOSTNAME").unwrap_or_default(), - user_name: std::env::var("USER").unwrap_or_default(), - cpu: CpuInfo { features: cpu_features(), cpuid, vendor, codename, ..Default::default() }, - ..Default::default() - } -} +//! Static system information for the Main window tree and the System Summary. +//! +//! Queried once at startup on a background thread (WMI/COM on Windows; minimal +//! fallbacks elsewhere). Anything a source can't provide stays `None` and the +//! UI renders "—" — honest placeholders until the native engine (SMBus SPD, +//! CPUID, NVML/ADL) fills them in. + +use std::sync::{Arc, RwLock}; + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct CpuInfo { + pub name: String, + pub cores: Option, + pub threads: Option, + pub base_clock_mhz: Option, + pub max_clock_mhz: Option, + pub l2_kb: Option, + pub l3_kb: Option, + pub socket: Option, + /// CPUID(1).EAX signature, HWiNFO-style hex (e.g. "00A60F12"). + pub cpuid: String, + /// Best-effort microarchitecture codename (e.g. "Raphael (Zen 4)"). + pub codename: String, + pub vendor: String, + /// ISA feature names detected at runtime (for the Summary features grid). + pub features: Vec<(&'static str, bool)>, +} + +/// Raw CPUID(1).EAX signature + vendor + codename, computed on x86_64. +fn cpuid_info() -> (String, String, String) { + #[cfg(target_arch = "x86_64")] + { + use core::arch::x86_64::__cpuid; + // __cpuid is safe on x86_64 (CPUID is always available). + let vendor_leaf = __cpuid(0); + let mut vbytes = Vec::new(); + vbytes.extend_from_slice(&vendor_leaf.ebx.to_le_bytes()); + vbytes.extend_from_slice(&vendor_leaf.edx.to_le_bytes()); + vbytes.extend_from_slice(&vendor_leaf.ecx.to_le_bytes()); + let vendor = String::from_utf8_lossy(&vbytes).to_string(); + + let leaf1 = __cpuid(1); + let eax = leaf1.eax; + let base_family = (eax >> 8) & 0xf; + let ext_family = (eax >> 20) & 0xff; + let family = if base_family == 0xf { base_family + ext_family } else { base_family }; + let base_model = (eax >> 4) & 0xf; + let ext_model = (eax >> 16) & 0xf; + let model = (ext_model << 4) | base_model; + + let codename = codename_for(&vendor, family, model); + (format!("{eax:08X}"), vendor, codename) + } + // Apple Silicon has no CPUID. The nearest equivalents are the board id + // (`hw.model`, e.g. "Mac17,3") and the SoC name from the brand string, so + // report those rather than leaving the Summary window blank. + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + { + let vendor = if sysctl_string("machdep.cpu.brand_string") + .is_some_and(|b| b.starts_with("Apple")) + { + "Apple".to_string() + } else { + String::new() + }; + (String::new(), vendor, sysctl_string("hw.model").unwrap_or_default()) + } + #[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "macos"))))] + { + (String::new(), String::new(), String::new()) + } +} + +/// Coarse codename map for recent AMD/Intel desktop parts (best effort). +#[allow(dead_code)] +fn codename_for(vendor: &str, family: u32, model: u32) -> String { + if vendor.contains("AuthenticAMD") { + match (family, model) { + (0x19, 0x60..=0x6f) => "Raphael (Zen 4)", + (0x19, 0x70..=0x7f) => "Phoenix (Zen 4)", + (0x19, 0x40..=0x4f) => "Rembrandt (Zen 3+)", + (0x19, 0x20..=0x2f) => "Vermeer (Zen 3)", + (0x19, 0x50..=0x5f) => "Cezanne (Zen 3)", + (0x1a, _) => "Granite Ridge (Zen 5)", + (0x17, _) => "Matisse/Renoir (Zen 2)", + _ => "", + } + .to_string() + } else if vendor.contains("GenuineIntel") { + match family { + 0x6 => "Intel Core", + _ => "", + } + .to_string() + } else { + String::new() + } +} + +/// SMBIOS *Memory Device* (structure type 17) memory-type code → display name. +/// +/// Codes are the DMTF SMBIOS specification's, cross-checked against +/// dmidecode's `dmi_memory_device_type` table, which runs from `0x01` to +/// `0x24`. +/// +/// This used to decode exactly three values — DDR3, DDR4, DDR5 — and answer +/// `"DRAM"` for everything else. The Summary appends " SDRAM" to whatever it +/// gets, so the fallback rendered as the literal string "DRAM SDRAM". Every +/// soldered-memory machine reports an LPDDR code, so that was most current +/// laptop hardware. +/// +/// A code the table does not know is reported as `Unknown (type 0x??)` rather +/// than guessed at: the raw code is what lets someone look it up, and DMTF +/// assigns new ones as memory generations ship. +#[allow(dead_code)] // Only reachable from the Windows WMI path. +fn smbios_memory_type(code: u32) -> String { + let name = match code { + 0x01 | 0x02 => "Unknown", // "Other" and "Unknown" are both non-answers. + 0x03 => "DRAM", + 0x04 => "EDRAM", + 0x05 => "VRAM", + 0x06 => "SRAM", + 0x07 => "RAM", + 0x0D => "CDRAM", + 0x0E => "3DRAM", + 0x0F => "SDRAM", + 0x10 => "SGRAM", + 0x11 => "RDRAM", + 0x12 => "DDR", + 0x13 => "DDR2", + 0x14 => "DDR2 FB-DIMM", + 0x18 => "DDR3", + 0x19 => "FBD2", + 0x1A => "DDR4", + 0x1B => "LPDDR", + 0x1C => "LPDDR2", + 0x1D => "LPDDR3", + 0x1E => "LPDDR4", + 0x1F => "Logical non-volatile device", + 0x20 => "HBM", + 0x21 => "HBM2", + 0x22 => "DDR5", + 0x23 => "LPDDR5", + 0x24 => "HBM3", + _ => return format!("Unknown (type {code:#04X})"), + }; + name.to_string() +} + +/// How the Summary labels a module: "DDR5" becomes "DDR5 SDRAM", but "HBM3" +/// and "Unknown" are left alone. +/// +/// The suffix used to be appended unconditionally, which is where "DRAM SDRAM" +/// came from. Only the DDR and LPDDR families are synchronous DRAM in the +/// sense that suffix means. +#[allow(dead_code)] // Only the GUI renders this; headless builds don't link it. +pub fn memory_type_label(memory_type: &str) -> String { + if memory_type.starts_with("DDR") || memory_type.starts_with("LPDDR") { + format!("{memory_type} SDRAM") + } else { + memory_type.to_string() + } +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct BoardInfo { + pub product: String, + pub manufacturer: String, + pub bios_version: String, + pub bios_date: String, +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct MemoryModule { + pub bank: String, + pub manufacturer: String, + pub part_number: String, + pub capacity_gb: f64, + pub speed_mts: Option, + pub configured_speed_mts: Option, + pub voltage_mv: Option, + pub memory_type: String, +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct GpuInfo { + pub name: String, + /// WMI AdapterRAM (u32, capped at 4 GB) — kept for the native engine to + /// replace with NVML/ADL truth; not displayed while unreliable. + #[allow(dead_code)] + pub vram_gb: Option, + pub driver_version: String, +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct DriveInfo { + pub model: String, + pub interface: String, + pub size_gb: Option, +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct OsInfo { + pub caption: String, + pub build: String, + pub arch: String, + pub uefi_boot: Option, + pub secure_boot: Option, +} + +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct SystemInfo { + pub computer_name: String, + pub user_name: String, + pub cpu: CpuInfo, + pub board: BoardInfo, + pub memory_modules: Vec, + pub total_memory_gb: Option, + pub gpus: Vec, + pub drives: Vec, + pub os: OsInfo, +} + +/// Shared handle: `None` until the background query completes. +pub type SystemInfoHandle = Arc>>; + +/// Whether this process is running elevated (`Some(true/false)` on Windows, +/// `None` elsewhere). Reliable and independent of the sidecar — the sidecar is +/// our child, so it inherits our elevation. +// Surfaced as the GUI's "Running as Administrator" badge. +#[allow(dead_code)] +pub fn is_elevated() -> Option { + #[cfg(windows)] + { + #[repr(C)] + struct TokenElevation { + token_is_elevated: u32, + } + const TOKEN_QUERY: u32 = 0x0008; + const TOKEN_ELEVATION_CLASS: i32 = 20; // TokenElevation + + #[link(name = "advapi32")] + extern "system" { + fn OpenProcessToken(process: isize, desired: u32, handle: *mut isize) -> i32; + fn GetTokenInformation( + token: isize, + class: i32, + info: *mut core::ffi::c_void, + len: u32, + ret_len: *mut u32, + ) -> i32; + } + extern "system" { + fn GetCurrentProcess() -> isize; + fn CloseHandle(h: isize) -> i32; + } + + unsafe { + let mut token: isize = 0; + if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 { + return None; + } + let mut elevation = TokenElevation { token_is_elevated: 0 }; + let mut ret_len = 0u32; + let ok = GetTokenInformation( + token, + TOKEN_ELEVATION_CLASS, + &mut elevation as *mut _ as *mut core::ffi::c_void, + core::mem::size_of::() as u32, + &mut ret_len, + ); + CloseHandle(token); + if ok == 0 { + None + } else { + Some(elevation.token_is_elevated != 0) + } + } + } + // Reported for the status badge only. Nothing on macOS *needs* root: the + // IOKit backend reads every sensor unprivileged, so no feature is gated on + // this (see ui/settings_dialog.rs). + #[cfg(target_os = "macos")] + { + Some(unsafe { libc::geteuid() } == 0) + } + #[cfg(not(any(windows, target_os = "macos")))] + { + None + } +} + +// ---- sysctl helpers (macOS) --------------------------------------------- + +/// Read a string-valued sysctl by name. `None` if the key doesn't exist — +/// keys come and go between macOS releases, so every caller must tolerate it. +#[cfg(target_os = "macos")] +pub(crate) fn sysctl_string(name: &str) -> Option { + let cname = std::ffi::CString::new(name).ok()?; + let mut len = 0usize; + // First call with a null buffer asks for the required size. + if unsafe { + libc::sysctlbyname(cname.as_ptr(), std::ptr::null_mut(), &mut len, std::ptr::null_mut(), 0) + } != 0 + || len == 0 + { + return None; + } + let mut buf = vec![0u8; len]; + if unsafe { + libc::sysctlbyname( + cname.as_ptr(), + buf.as_mut_ptr().cast(), + &mut len, + std::ptr::null_mut(), + 0, + ) + } != 0 + { + return None; + } + buf.truncate(len); + // sysctl strings are NUL-terminated; drop the terminator and anything after. + if let Some(nul) = buf.iter().position(|&b| b == 0) { + buf.truncate(nul); + } + let s = String::from_utf8_lossy(&buf).trim().to_string(); + (!s.is_empty()).then_some(s) +} + +/// Read an integer-valued sysctl. Handles both the 4-byte and 8-byte widths the +/// kernel uses (`hw.ncpu` is 32-bit, `hw.memsize` is 64-bit). +#[cfg(target_os = "macos")] +pub(crate) fn sysctl_u64(name: &str) -> Option { + let cname = std::ffi::CString::new(name).ok()?; + let mut value = 0u64; + let mut len = std::mem::size_of::(); + if unsafe { + libc::sysctlbyname( + cname.as_ptr(), + (&mut value as *mut u64).cast(), + &mut len, + std::ptr::null_mut(), + 0, + ) + } != 0 + { + return None; + } + match len { + 8 => Some(value), + // The kernel wrote only the low 4 bytes; the upper half is our zeroed + // initialiser, so mask rather than trusting the whole u64. + 4 => Some(value & 0xffff_ffff), + _ => None, + } +} + +/// Synchronous `query()` for tests and diagnostics — `spawn_query` returns a +/// handle that only fills in later, which is awkward to assert against. +/// +/// Only the macOS system-profile test consumes it today; `allow(dead_code)` +/// keeps `clippy -D warnings` green on platforms that have no caller yet. +#[cfg(test)] +#[allow(dead_code)] +pub fn query_for_test() -> SystemInfo { + query() +} + +/// Kick off the (slow) WMI enumeration without blocking the UI. +pub fn spawn_query() -> SystemInfoHandle { + let handle: SystemInfoHandle = Arc::new(RwLock::new(None)); + let sink = handle.clone(); + std::thread::spawn(move || { + let info = query(); + if let Ok(mut slot) = sink.write() { + *slot = Some(info); + } + }); + handle +} + +fn cpu_features() -> Vec<(&'static str, bool)> { + #[cfg(target_arch = "x86_64")] + { + vec![ + ("MMX", is_x86_feature_detected!("mmx")), + ("SSE", is_x86_feature_detected!("sse")), + ("SSE2", is_x86_feature_detected!("sse2")), + ("SSE3", is_x86_feature_detected!("sse3")), + ("SSSE3", is_x86_feature_detected!("ssse3")), + ("SSE4.1", is_x86_feature_detected!("sse4.1")), + ("SSE4.2", is_x86_feature_detected!("sse4.2")), + ("SSE4A", is_x86_feature_detected!("sse4a")), + ("AVX", is_x86_feature_detected!("avx")), + ("AVX2", is_x86_feature_detected!("avx2")), + ("AVX-512F", is_x86_feature_detected!("avx512f")), + ("FMA", is_x86_feature_detected!("fma")), + ("BMI1", is_x86_feature_detected!("bmi1")), + ("BMI2", is_x86_feature_detected!("bmi2")), + ("AES-NI", is_x86_feature_detected!("aes")), + ("SHA", is_x86_feature_detected!("sha")), + ("RDRAND", is_x86_feature_detected!("rdrand")), + ("RDSEED", is_x86_feature_detected!("rdseed")), + ("POPCNT", is_x86_feature_detected!("popcnt")), + ("F16C", is_x86_feature_detected!("f16c")), + ] + } + // Apple Silicon: the kernel publishes ~80 `hw.optional.arm.FEAT_*` flags. + // Curated rather than enumerated so the grid stays readable and the labels + // stay `&'static str` — the full list is mostly MTE/SME sub-variants. + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + { + const FEATURES: &[(&str, &str)] = &[ + ("NEON", "hw.optional.arm.AdvSIMD"), + ("FP16", "hw.optional.arm.FEAT_FP16"), + ("BF16", "hw.optional.arm.FEAT_BF16"), + ("I8MM", "hw.optional.arm.FEAT_I8MM"), + ("DotProd", "hw.optional.arm.FEAT_DotProd"), + ("FHM", "hw.optional.arm.FEAT_FHM"), + ("CRC32", "hw.optional.arm.FEAT_CRC32"), + ("AES", "hw.optional.arm.FEAT_AES"), + ("PMULL", "hw.optional.arm.FEAT_PMULL"), + ("SHA1", "hw.optional.arm.FEAT_SHA1"), + ("SHA256", "hw.optional.arm.FEAT_SHA256"), + ("SHA3", "hw.optional.arm.FEAT_SHA3"), + ("SHA512", "hw.optional.arm.FEAT_SHA512"), + ("LSE", "hw.optional.arm.FEAT_LSE"), + ("LSE2", "hw.optional.arm.FEAT_LSE2"), + ("RDM", "hw.optional.arm.FEAT_RDM"), + ("JSCVT", "hw.optional.arm.FEAT_JSCVT"), + ("FCMA", "hw.optional.arm.FEAT_FCMA"), + ("LRCPC", "hw.optional.arm.FEAT_LRCPC"), + ("PAuth", "hw.optional.arm.FEAT_PAuth"), + ("BTI", "hw.optional.arm.FEAT_BTI"), + ("MTE", "hw.optional.arm.FEAT_MTE"), + ("DIT", "hw.optional.arm.FEAT_DIT"), + ("ECV", "hw.optional.arm.FEAT_ECV"), + ("SME", "hw.optional.arm.FEAT_SME"), + ("SME2", "hw.optional.arm.FEAT_SME2"), + ("SSBS", "hw.optional.arm.FEAT_SSBS"), + ("SPECRES", "hw.optional.arm.FEAT_SPECRES"), + ]; + FEATURES + .iter() + .map(|(label, key)| (*label, sysctl_u64(key).unwrap_or(0) != 0)) + .collect() + } + #[cfg(not(any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "macos"))))] + { + Vec::new() + } +} + +#[cfg(windows)] +fn query() -> SystemInfo { + use std::collections::HashMap; + use wmi::{Variant, WMIConnection}; + + let mut info = SystemInfo { + computer_name: std::env::var("COMPUTERNAME").unwrap_or_default(), + user_name: std::env::var("USERNAME").unwrap_or_default(), + ..Default::default() + }; + info.cpu.features = cpu_features(); + let (cpuid, vendor, codename) = cpuid_info(); + info.cpu.cpuid = cpuid; + info.cpu.vendor = vendor; + info.cpu.codename = codename; + + let Ok(wmi) = WMIConnection::new() else { return info }; + + type Row = HashMap; + + let s = |v: Option<&Variant>| -> String { + match v { + Some(Variant::String(x)) => x.trim().to_string(), + _ => String::new(), + } + }; + let u = |v: Option<&Variant>| -> Option { + match v { + Some(Variant::UI4(x)) => Some(*x), + Some(Variant::I4(x)) => u32::try_from(*x).ok(), + Some(Variant::UI2(x)) => Some(*x as u32), + Some(Variant::String(x)) => x.parse().ok(), + _ => None, + } + }; + let u64v = |v: Option<&Variant>| -> Option { + match v { + Some(Variant::UI8(x)) => Some(*x), + Some(Variant::I8(x)) => u64::try_from(*x).ok(), + Some(Variant::UI4(x)) => Some(*x as u64), + Some(Variant::String(x)) => x.parse().ok(), + _ => None, + } + }; + + if let Ok(rows) = wmi.raw_query::( + "SELECT Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed, L2CacheSize, L3CacheSize, SocketDesignation FROM Win32_Processor", + ) { + if let Some(r) = rows.first() { + info.cpu.name = s(r.get("Name")); + info.cpu.cores = u(r.get("NumberOfCores")); + info.cpu.threads = u(r.get("NumberOfLogicalProcessors")); + info.cpu.max_clock_mhz = u(r.get("MaxClockSpeed")); + info.cpu.base_clock_mhz = u(r.get("MaxClockSpeed")); + info.cpu.l2_kb = u(r.get("L2CacheSize")); + info.cpu.l3_kb = u(r.get("L3CacheSize")); + info.cpu.socket = Some(s(r.get("SocketDesignation"))).filter(|x| !x.is_empty()); + } + } + + if let Ok(rows) = wmi.raw_query::("SELECT Product, Manufacturer FROM Win32_BaseBoard") { + if let Some(r) = rows.first() { + info.board.product = s(r.get("Product")); + info.board.manufacturer = s(r.get("Manufacturer")); + } + } + if let Ok(rows) = wmi.raw_query::("SELECT SMBIOSBIOSVersion, ReleaseDate FROM Win32_BIOS") { + if let Some(r) = rows.first() { + info.board.bios_version = s(r.get("SMBIOSBIOSVersion")); + let date = s(r.get("ReleaseDate")); + // WMI CIM_DATETIME: yyyymmddHHMMSS… → mm/dd/yyyy like HWiNFO shows. + if date.len() >= 8 { + info.board.bios_date = format!("{}/{}/{}", &date[4..6], &date[6..8], &date[0..4]); + } + } + } + + if let Ok(rows) = wmi.raw_query::( + "SELECT BankLabel, DeviceLocator, Manufacturer, PartNumber, Capacity, Speed, ConfiguredClockSpeed, ConfiguredVoltage, SMBIOSMemoryType FROM Win32_PhysicalMemory", + ) { + let mut total = 0.0; + for r in &rows { + let capacity_gb = u64v(r.get("Capacity")).map(|b| b as f64 / (1u64 << 30) as f64).unwrap_or(0.0); + total += capacity_gb; + // An absent field is left blank; a present one is decoded, so an + // unrecognised code shows as `Unknown (type 0x??)` rather than + // being flattened into a wrong name. + let mem_type = u(r.get("SMBIOSMemoryType")) + .map(smbios_memory_type) + .unwrap_or_default(); + info.memory_modules.push(MemoryModule { + bank: { + let bank = s(r.get("BankLabel")); + let loc = s(r.get("DeviceLocator")); + if bank.is_empty() { loc } else { format!("{bank}/{loc}") } + }, + manufacturer: s(r.get("Manufacturer")), + part_number: s(r.get("PartNumber")), + capacity_gb, + speed_mts: u(r.get("Speed")), + configured_speed_mts: u(r.get("ConfiguredClockSpeed")), + voltage_mv: u(r.get("ConfiguredVoltage")), + memory_type: mem_type.to_string(), + }); + } + if total > 0.0 { + info.total_memory_gb = Some(total); + } + } + + if let Ok(rows) = wmi.raw_query::("SELECT Name, AdapterRAM, DriverVersion FROM Win32_VideoController") { + for r in &rows { + info.gpus.push(GpuInfo { + name: s(r.get("Name")), + vram_gb: u64v(r.get("AdapterRAM")).map(|b| b as f64 / (1u64 << 30) as f64), + driver_version: s(r.get("DriverVersion")), + }); + } + } + + if let Ok(rows) = wmi.raw_query::("SELECT Model, InterfaceType, Size FROM Win32_DiskDrive") { + for r in &rows { + info.drives.push(DriveInfo { + model: s(r.get("Model")), + interface: s(r.get("InterfaceType")), + size_gb: u64v(r.get("Size")).map(|b| b as f64 / 1_000_000_000.0), + }); + } + } + + if let Ok(rows) = wmi.raw_query::("SELECT Caption, BuildNumber, OSArchitecture FROM Win32_OperatingSystem") { + if let Some(r) = rows.first() { + info.os.caption = s(r.get("Caption")); + info.os.build = s(r.get("BuildNumber")); + info.os.arch = s(r.get("OSArchitecture")); + } + } + + // Secure Boot / UEFI: registry flag (no clean WMI class for it). + info.os.secure_boot = read_secure_boot(); + info.os.uefi_boot = info.os.secure_boot.map(|_| true); + + info +} + +#[cfg(windows)] +fn read_secure_boot() -> Option { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + let out = std::process::Command::new("reg") + .args([ + "query", + r"HKLM\SYSTEM\CurrentControlSet\Control\SecureBoot\State", + "/v", + "UEFISecureBootEnabled", + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .ok()?; + let text = String::from_utf8_lossy(&out.stdout); + if text.contains("0x1") { + Some(true) + } else if text.contains("0x0") { + Some(false) + } else { + None + } +} + +#[cfg(target_os = "macos")] +fn query() -> SystemInfo { + let (cpuid, vendor, codename) = cpuid_info(); + + // Apple Silicon is heterogeneous: perflevel0 is the fast cluster (named + // "Performance" through M4, "Super" on M5 — read the name, don't hardcode + // it) and perflevel1 the efficiency cluster. Sum both for the core count, + // and report the layout in `socket` since there is no socket to speak of. + let mut cluster_desc = Vec::new(); + let mut physical = 0u32; + for level in 0..4 { + let Some(count) = sysctl_u64(&format!("hw.perflevel{level}.physicalcpu")) else { + break; + }; + physical += count as u32; + let name = sysctl_string(&format!("hw.perflevel{level}.name")) + .unwrap_or_else(|| format!("level{level}")); + cluster_desc.push(format!("{count} {name}")); + } + // Fall back to the flat count on any Mac that doesn't publish perflevels. + let cores = if physical > 0 { Some(physical) } else { sysctl_u64("hw.physicalcpu").map(|v| v as u32) }; + + let total_memory_gb = sysctl_u64("hw.memsize").map(|b| b as f64 / (1024.0 * 1024.0 * 1024.0)); + + // The SoC is soldered, so there are no per-DIMM SPD entries to enumerate; + // present the unified memory as a single honest module. + let memory_modules = total_memory_gb + .map(|gb| { + vec![MemoryModule { + bank: "Unified Memory".into(), + manufacturer: "Apple".into(), + capacity_gb: gb, + memory_type: "LPDDR (on-package)".into(), + ..Default::default() + }] + }) + .unwrap_or_default(); + + // The integrated GPU shares the SoC; name it after the chip and its core + // count rather than inventing a discrete-adapter identity. + let chip = sysctl_string("machdep.cpu.brand_string").unwrap_or_default(); + let (gpu_cores, metal_driver) = crate::source::macos::sysprofile::gpu_identity(); + let gpus = if chip.is_empty() { + Vec::new() + } else { + let name = match gpu_cores { + Some(cores) => format!("{chip} GPU ({cores} cores)"), + None => format!("{chip} GPU"), + }; + vec![GpuInfo { + name, + // Unified memory — there is no separate VRAM pool to report, and + // quoting total system RAM here would be misleading. + vram_gb: None, + driver_version: metal_driver.unwrap_or_default(), + }] + }; + + // Internal SSD(s), so the Summary's Drives panel isn't blank. + const GB: f64 = 1024.0 * 1024.0 * 1024.0; + let drives = crate::source::macos::sysprofile::drives() + .into_iter() + .map(|(model, bytes)| DriveInfo { + model, + interface: "NVMe".into(), + size_gb: bytes.map(|b| b as f64 / GB), + }) + .collect(); + + // Performance-cluster DVFS states, for the Base/Max Clock rows. + let p_states = + crate::source::macos::dvfs::frequencies_mhz(crate::source::macos::dvfs::Block::Pcpu); + + let os_version = sysctl_string("kern.osproductversion").unwrap_or_default(); + + SystemInfo { + computer_name: sysctl_string("kern.hostname").unwrap_or_default(), + user_name: std::env::var("USER").unwrap_or_default(), + cpu: CpuInfo { + name: chip, + cores, + // Apple Silicon has no SMT: one thread per physical core. + threads: sysctl_u64("hw.logicalcpu").map(|v| v as u32), + // There is no fixed "base clock" on Apple Silicon; the closest + // honest equivalents are the bottom and top of the performance + // cluster's DVFS table. + base_clock_mhz: p_states.first().map(|mhz| *mhz as u32), + max_clock_mhz: p_states.last().map(|mhz| *mhz as u32), + // hw.l2cachesize reports the *efficiency* cluster (6 MB here). + // The headline figure is the performance cluster's L2 + // (hw.perflevel0.l2cachesize, 16 MB), so prefer that. + l2_kb: sysctl_u64("hw.perflevel0.l2cachesize") + .or_else(|| sysctl_u64("hw.l2cachesize")) + .map(|b| (b / 1024) as u32), + // Apple Silicon has no per-core L3; the system-level cache is not + // published anywhere readable, so this stays honestly blank. + l3_kb: None, + socket: (!cluster_desc.is_empty()).then(|| cluster_desc.join(" + ")), + features: cpu_features(), + cpuid, + vendor, + codename, + }, + board: BoardInfo { + // Prefer the marketing name ("MacBook Air (13-inch, M5)") over the + // bare board id ("Mac17,3"), which is already shown as the codename. + product: crate::source::macos::sysprofile::product_name() + .or_else(|| sysctl_string("hw.model")) + .unwrap_or_default(), + manufacturer: "Apple Inc.".into(), + // Apple Silicon boots via iBoot, so the closest thing to a BIOS + // version is the boot firmware revision. + bios_version: crate::source::macos::sysprofile::firmware_version().unwrap_or_default(), + // No firmware build date is published anywhere in IOKit; leave it + // blank rather than guessing from the version string. + bios_date: String::new(), + }, + memory_modules, + total_memory_gb, + gpus, + drives, + os: OsInfo { + caption: if os_version.is_empty() { + "macOS".into() + } else { + format!("macOS {os_version}") + }, + build: sysctl_string("kern.osversion").unwrap_or_default(), + arch: std::env::consts::ARCH.to_string(), + // Apple Silicon boots via iBoot, not UEFI, and Secure Boot state + // lives in a different subsystem — leave both indeterminate rather + // than asserting something false. + uefi_boot: None, + secure_boot: None, + }, + } +} + +#[cfg(target_os = "linux")] +fn query() -> SystemInfo { + use std::fs; + let (cpuid, vendor, codename) = cpuid_info(); + let mut info = SystemInfo { + computer_name: std::env::var("HOSTNAME").unwrap_or_default(), + user_name: std::env::var("USER").unwrap_or_default(), + cpu: CpuInfo { features: cpu_features(), cpuid, vendor, codename, ..Default::default() }, + ..Default::default() + }; + + // Read CPU Model Name and Cores from /proc/cpuinfo + if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") { + let mut model_name = String::new(); + let mut logical_count = 0u32; + for line in cpuinfo.lines() { + if line.starts_with("model name") { + if let Some(val) = line.split(':').nth(1) { + if model_name.is_empty() { + model_name = val.trim().to_string(); + } + } + } + if line.starts_with("processor") { + logical_count += 1; + } + } + if !model_name.is_empty() { + info.cpu.name = model_name; + } + if logical_count > 0 { + info.cpu.threads = Some(logical_count); + info.cpu.cores = Some(logical_count); // best-effort fallback + } + } + + // Read Motherboard / DMI Info from /sys/class/dmi/id + if let Ok(product) = fs::read_to_string("/sys/class/dmi/id/board_name") { + info.board.product = product.trim().to_string(); + } + if let Ok(vendor) = fs::read_to_string("/sys/class/dmi/id/board_vendor") { + info.board.manufacturer = vendor.trim().to_string(); + } + if let Ok(version) = fs::read_to_string("/sys/class/dmi/id/bios_version") { + info.board.bios_version = version.trim().to_string(); + } + if let Ok(date) = fs::read_to_string("/sys/class/dmi/id/bios_date") { + info.board.bios_date = date.trim().to_string(); + } + + // Read RAM Total from /proc/meminfo + if let Ok(meminfo) = fs::read_to_string("/proc/meminfo") { + for line in meminfo.lines() { + if line.starts_with("MemTotal:") { + if let Some(kb_str) = line.split_whitespace().nth(1) { + if let Ok(kb) = kb_str.parse::() { + info.total_memory_gb = Some(kb / (1024.0 * 1024.0)); + } + } + } + } + } + + // Read OS info from /etc/os-release + if let Ok(os_release) = fs::read_to_string("/etc/os-release") { + for line in os_release.lines() { + if line.starts_with("PRETTY_NAME=") { + let name = line.trim_start_matches("PRETTY_NAME=").trim_matches('"'); + info.os.caption = name.to_string(); + } + } + } + + info +} + +#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] +fn query() -> SystemInfo { + let (cpuid, vendor, codename) = cpuid_info(); + SystemInfo { + computer_name: std::env::var("HOSTNAME").unwrap_or_default(), + user_name: std::env::var("USER").unwrap_or_default(), + cpu: CpuInfo { features: cpu_features(), cpuid, vendor, codename, ..Default::default() }, + ..Default::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Codes are the DMTF SMBIOS values, cross-checked against dmidecode's + // `dmi_memory_device_type` table. These run on every platform, since the + // decoder takes the raw code rather than reading WMI. + + #[test] + fn the_three_codes_that_already_worked_still_decode_the_same_way() { + assert_eq!(smbios_memory_type(24), "DDR3"); + assert_eq!(smbios_memory_type(26), "DDR4"); + assert_eq!(smbios_memory_type(34), "DDR5"); + } + + #[test] + fn soldered_laptop_memory_is_named_rather_than_flattened_to_dram() { + // The reason for the change: every one of these used to fall through + // to "DRAM" and render as "DRAM SDRAM". + assert_eq!(smbios_memory_type(0x1B), "LPDDR"); + assert_eq!(smbios_memory_type(0x1C), "LPDDR2"); + assert_eq!(smbios_memory_type(0x1D), "LPDDR3"); + assert_eq!(smbios_memory_type(0x1E), "LPDDR4"); + assert_eq!(smbios_memory_type(0x23), "LPDDR5"); + } + + #[test] + fn stacked_memory_decodes_too() { + assert_eq!(smbios_memory_type(0x20), "HBM"); + assert_eq!(smbios_memory_type(0x21), "HBM2"); + assert_eq!(smbios_memory_type(0x24), "HBM3"); + } + + #[test] + fn an_unassigned_code_reports_the_raw_value_instead_of_guessing() { + // 0x25 is past the last code DMTF has assigned. Showing the number is + // what lets someone look it up. + assert_eq!(smbios_memory_type(0x25), "Unknown (type 0x25)"); + assert_eq!(smbios_memory_type(0xFF), "Unknown (type 0xFF)"); + } + + #[test] + fn smbios_other_and_unknown_are_both_reported_as_unknown() { + assert_eq!(smbios_memory_type(0x01), "Unknown"); + assert_eq!(smbios_memory_type(0x02), "Unknown"); + } + + #[test] + fn the_sdram_suffix_is_only_added_where_it_means_something() { + assert_eq!(memory_type_label("DDR5"), "DDR5 SDRAM"); + assert_eq!(memory_type_label("LPDDR5"), "LPDDR5 SDRAM"); + // These are the ones that used to produce nonsense. + assert_eq!(memory_type_label("HBM3"), "HBM3"); + assert_eq!(memory_type_label("Unknown"), "Unknown"); + assert_eq!(memory_type_label("Unknown (type 0x25)"), "Unknown (type 0x25)"); + assert_eq!(memory_type_label(""), ""); + } +} diff --git a/app/src/ui/summary_window.rs b/app/src/ui/summary_window.rs index 5b1ed9fa..18e537ab 100644 --- a/app/src/ui/summary_window.rs +++ b/app/src/ui/summary_window.rs @@ -1,395 +1,395 @@ -//! HWiNFO-style "System Summary" window: CPU / Motherboard / Memory / GPU / -//! OS / Drives panel grid with the ISA features chip-grid and an Operating -//! Point table fed by live sensors. - -use eframe::egui::{self, RichText}; - -use super::widgets::{chip, info_row, panel}; -use super::{Palette, Shared}; -use crate::model::{Hardware, HardwareType, SensorType}; - -pub fn show(ui: &mut egui::Ui, s: &Shared) { - super::handle_close(ui, &s.windows.summary); - let pal = s.palette(); - let info = s.sysinfo.read().ok().and_then(|i| i.clone()); - let frame = s.frame(); - let tree = &frame.tree; - - egui::CentralPanel::default() - .frame( - egui::Frame::new() - .fill(pal.bg) - .inner_margin(egui::Margin::same(8)), - ) - .show(ui, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - let Some(i) = info else { - ui.label(RichText::new("Enumerating system…").color(pal.text_dim)); - return; - }; - - ui.columns(3, |cols| { - // ---- CPU ------------------------------------------------ - cpu_panel(&mut cols[0], &i, tree, &pal); - // ---- Motherboard + Memory ------------------------------ - board_memory_panels(&mut cols[1], &i, &pal); - // ---- GPU + OS + Drives --------------------------------- - gpu_os_drives_panels(&mut cols[2], &i, tree, &pal); - }); - }); - }); -} - -fn cpu_panel(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { - panel(ui, "CPU", pal, |ui| { - // Vendor text badge (no trademarked logos). - let vendor = if i.cpu.name.to_uppercase().contains("AMD") { - "AMD" - } else if i.cpu.name.to_uppercase().contains("INTEL") { - "INTEL" - } else { - "CPU" - }; - ui.horizontal(|ui| { - egui::Frame::new() - .fill(pal.bg_header) - .corner_radius(3) - .inner_margin(egui::Margin::symmetric(10, 8)) - .show(ui, |ui| { - ui.label(RichText::new(vendor).color(pal.accent).size(15.0).strong()); - }); - ui.vertical(|ui| { - ui.label(RichText::new(&i.cpu.name).color(pal.text).size(12.0).strong()); - ui.label( - RichText::new(i.cpu.socket.as_deref().unwrap_or("—")) - .color(pal.text_dim) - .size(10.5), - ); - }); - }); - ui.add_space(4.0); - - let cores = i - .cpu - .cores - .map(|c| format!("{c} / {}", i.cpu.threads.unwrap_or(c))) - .unwrap_or_default(); - info_row(ui, "Cores / Threads:", &cores, pal); - info_row( - ui, - "L2 Cache:", - &i.cpu.l2_kb.map(|k| format!("{} KB", k)).unwrap_or_default(), - pal, - ); - info_row( - ui, - "L3 Cache:", - &i.cpu.l3_kb.map(|k| format!("{} MB", k / 1024)).unwrap_or_default(), - pal, - ); - info_row(ui, "Codename:", &i.cpu.codename, pal); - info_row(ui, "CPUID:", &i.cpu.cpuid, pal); - info_row( - ui, - "Package Power:", - &cpu_sensor(tree, SensorType::Power, "package") - .map(|v| format!("{v:.1} W")) - .unwrap_or_default(), - pal, - ); - - ui.add_space(4.0); - ui.label(RichText::new("Features").color(pal.text_dim).size(10.5)); - // Fixed rows of 5 — deterministic wrap regardless of column width. - for row in i.cpu.features.chunks(5) { - ui.horizontal(|ui| { - for (name, on) in row { - chip(ui, name, *on, pal); - } - }); - } - - ui.add_space(6.0); - ui.label(RichText::new("Operating Point").color(pal.text_dim).size(10.5)); - operating_point_table(ui, i, tree, pal); - }); -} - -/// Min/Base/Boost/Avg clock table from WMI base clock + live core clocks/VIDs. -fn operating_point_table(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { - let clocks = collect_cpu(tree, SensorType::Clock, "core"); - let vids = collect_cpu(tree, SensorType::Voltage, "vid"); - let cur_avg = mean(&clocks); - let cur_max = clocks.iter().copied().fold(f32::NAN, f32::max); - let vid = mean(&vids); - - egui::Grid::new("op_table") - .num_columns(3) - .spacing([12.0, 2.0]) - .show(ui, |ui| { - let head = |ui: &mut egui::Ui, t: &str| { - ui.label(RichText::new(t).color(pal.text_dim).size(10.5).strong()); - }; - head(ui, ""); - head(ui, "Clock"); - head(ui, "VID"); - ui.end_row(); - - let row = |ui: &mut egui::Ui, name: &str, clock: Option, vid: Option, pal: &Palette| { - ui.label(RichText::new(name).color(pal.text).size(10.5)); - ui.label( - RichText::new(clock.map(|c| format!("{c:.1} MHz")).unwrap_or("—".into())) - .color(pal.clockc) - .size(10.5) - .monospace(), - ); - ui.label( - RichText::new(vid.map(|v| format!("{v:.4} V")).unwrap_or("—".into())) - .color(pal.volt) - .size(10.5) - .monospace(), - ); - ui.end_row(); - }; - row(ui, "Base Clock", i.cpu.base_clock_mhz.map(|c| c as f32), None, pal); - row(ui, "Max Clock", Some(cur_max).filter(|v| v.is_finite()), None, pal); - row(ui, "Avg. Active Clock", cur_avg, vid, pal); - }); -} - -fn board_memory_panels(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, pal: &Palette) { - panel(ui, "Motherboard", pal, |ui| { - ui.label( - RichText::new(format!("{} {}", i.board.manufacturer, i.board.product)) - .color(pal.text) - .size(12.0) - .strong(), - ); - ui.add_space(2.0); - info_row(ui, "Chipset:", "", pal); // needs PCI enum — native engine - info_row(ui, "BIOS Version:", &i.board.bios_version, pal); - info_row(ui, "BIOS Date:", &i.board.bios_date, pal); - }); - - ui.add_space(6.0); - - panel(ui, "Memory", pal, |ui| { - info_row( - ui, - "Size:", - &i.total_memory_gb.map(|g| format!("{g:.0} GB")).unwrap_or_default(), - pal, - ); - let mem_type = i - .memory_modules - .first() - .map(|m| format!("{} SDRAM", m.memory_type)) - .unwrap_or_default(); - info_row(ui, "Type:", &mem_type, pal); - let clock = i - .memory_modules - .first() - .and_then(|m| m.configured_speed_mts.or(m.speed_mts)) - .map(|v| format!("{v} MT/s")) - .unwrap_or_default(); - info_row(ui, "Clock:", &clock, pal); - // Unified memory is a wide on-package bus, not a DIMM channel count — - // inferring "Single-Channel" from the one synthetic module would be - // plainly wrong. - let unified = i - .memory_modules - .first() - .is_some_and(|m| m.memory_type.contains("on-package")); - let mode = if unified { - "Unified" - } else { - match i.memory_modules.len() { - 2 => "Dual-Channel", - 4 => "Quad-Channel", - 1 => "Single-Channel", - _ => "", - } - }; - info_row(ui, "Mode:", mode, pal); - info_row(ui, "Timings:", "", pal); // needs SPD — native engine - - ui.add_space(4.0); - ui.label(RichText::new("Memory Modules").color(pal.text_dim).size(10.5)); - for m in &i.memory_modules { - egui::Frame::new() - .fill(pal.bg_header) - .corner_radius(2) - .inner_margin(egui::Margin::same(4)) - .show(ui, |ui| { - ui.label( - RichText::new(format!("{}: {} {}", m.bank, m.manufacturer, m.part_number)) - .color(pal.text) - .size(10.5), - ); - ui.label( - RichText::new(format!( - "{:.0} GB {} @ {} MT/s {}", - m.capacity_gb, - m.memory_type, - m.configured_speed_mts.or(m.speed_mts).unwrap_or(0), - m.voltage_mv - .map(|v| format!("{:.2} V", v as f32 / 1000.0)) - .unwrap_or_default(), - )) - .color(pal.text_dim) - .size(10.0), - ); - }); - ui.add_space(2.0); - } - }); -} - -fn gpu_os_drives_panels(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { - panel(ui, "GPU", pal, |ui| { - for (gi, g) in i.gpus.iter().enumerate() { - let vendor = if g.name.to_uppercase().contains("NVIDIA") { - "NVIDIA" - } else if g.name.to_uppercase().contains("AMD") || g.name.to_uppercase().contains("RADEON") { - "RADEON" - } else { - "GPU" - }; - ui.horizontal(|ui| { - egui::Frame::new() - .fill(pal.bg_header) - .corner_radius(3) - .inner_margin(egui::Margin::symmetric(8, 6)) - .show(ui, |ui| { - ui.label(RichText::new(vendor).color(pal.ok_badge).size(12.0).strong()); - }); - ui.vertical(|ui| { - ui.label(RichText::new(&g.name).color(pal.text).size(11.5).strong()); - // NOTE: WMI AdapterRAM is a u32 capped at 4 GB — showing it - // would be wrong for modern cards. VRAM comes with the - // native GPU engine (NVML/ADL). - ui.label( - RichText::new(format!("Driver {}", g.driver_version)) - .color(pal.text_dim) - .size(10.0), - ); - }); - }); - if gi + 1 < i.gpus.len() { - ui.add_space(3.0); - } - } - ui.add_space(4.0); - // Live GPU clocks from sensors. - let (core, mem) = gpu_live_clocks(tree); - info_row( - ui, - "GPU Clock:", - &core.map(|v| format!("{v:.1} MHz")).unwrap_or_default(), - pal, - ); - info_row( - ui, - "Memory Clock:", - &mem.map(|v| format!("{v:.1} MHz")).unwrap_or_default(), - pal, - ); - info_row(ui, "PCIe Link:", "", pal); // needs native engine - }); - - ui.add_space(6.0); - - panel(ui, "Operating System", pal, |ui| { - ui.label( - RichText::new(format!("{} ({})", i.os.caption, i.os.arch)) - .color(pal.text) - .size(11.0), - ); - info_row(ui, "Build:", &i.os.build, pal); - super::widgets::badge(ui, "UEFI Boot:", i.os.uefi_boot, pal); - super::widgets::badge(ui, "Secure Boot:", i.os.secure_boot, pal); - }); - - ui.add_space(6.0); - - panel(ui, "Drives", pal, |ui| { - for d in &i.drives { - ui.label( - RichText::new(format!( - "• {} [{}] {}", - d.model, - d.interface, - d.size_gb.map(|g| format!("{g:.0} GB")).unwrap_or_default() - )) - .color(pal.text) - .size(10.5), - ); - } - }); -} - -// ---- live-sensor helpers ------------------------------------------------ - -/// First CPU sensor of a type whose name contains `needle` (case-insensitive). -fn cpu_sensor(tree: &[Hardware], t: SensorType, needle: &str) -> Option { - for hw in tree { - if hw.hardware_type == HardwareType::Cpu { - for s in &hw.sensors { - if s.sensor_type == t && s.name.to_lowercase().contains(needle) { - return s.value; - } - } - } - } - None -} - -fn collect_cpu(tree: &[Hardware], t: SensorType, name_contains: &str) -> Vec { - let mut out = Vec::new(); - for hw in tree { - if hw.hardware_type == HardwareType::Cpu { - for s in &hw.sensors { - if s.sensor_type == t && s.name.to_lowercase().contains(name_contains) { - if let Some(v) = s.value { - out.push(v); - } - } - } - } - } - out -} - -fn gpu_live_clocks(tree: &[Hardware]) -> (Option, Option) { - let mut core = None; - let mut mem = None; - for hw in tree { - if matches!( - hw.hardware_type, - HardwareType::GpuNvidia - | HardwareType::GpuAti - | HardwareType::GpuIntel - | HardwareType::GpuApple - ) { - for s in &hw.sensors { - if s.sensor_type == SensorType::Clock { - let n = s.name.to_lowercase(); - if n.contains("core") && core.is_none() { - core = s.value; - } else if n.contains("memory") && mem.is_none() { - mem = s.value; - } - } - } - } - } - (core, mem) -} - -fn mean(v: &[f32]) -> Option { - if v.is_empty() { - None - } else { - Some(v.iter().sum::() / v.len() as f32) - } -} +//! HWiNFO-style "System Summary" window: CPU / Motherboard / Memory / GPU / +//! OS / Drives panel grid with the ISA features chip-grid and an Operating +//! Point table fed by live sensors. + +use eframe::egui::{self, RichText}; + +use super::widgets::{chip, info_row, panel}; +use super::{Palette, Shared}; +use crate::model::{Hardware, HardwareType, SensorType}; + +pub fn show(ui: &mut egui::Ui, s: &Shared) { + super::handle_close(ui, &s.windows.summary); + let pal = s.palette(); + let info = s.sysinfo.read().ok().and_then(|i| i.clone()); + let frame = s.frame(); + let tree = &frame.tree; + + egui::CentralPanel::default() + .frame( + egui::Frame::new() + .fill(pal.bg) + .inner_margin(egui::Margin::same(8)), + ) + .show(ui, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + let Some(i) = info else { + ui.label(RichText::new("Enumerating system…").color(pal.text_dim)); + return; + }; + + ui.columns(3, |cols| { + // ---- CPU ------------------------------------------------ + cpu_panel(&mut cols[0], &i, tree, &pal); + // ---- Motherboard + Memory ------------------------------ + board_memory_panels(&mut cols[1], &i, &pal); + // ---- GPU + OS + Drives --------------------------------- + gpu_os_drives_panels(&mut cols[2], &i, tree, &pal); + }); + }); + }); +} + +fn cpu_panel(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { + panel(ui, "CPU", pal, |ui| { + // Vendor text badge (no trademarked logos). + let vendor = if i.cpu.name.to_uppercase().contains("AMD") { + "AMD" + } else if i.cpu.name.to_uppercase().contains("INTEL") { + "INTEL" + } else { + "CPU" + }; + ui.horizontal(|ui| { + egui::Frame::new() + .fill(pal.bg_header) + .corner_radius(3) + .inner_margin(egui::Margin::symmetric(10, 8)) + .show(ui, |ui| { + ui.label(RichText::new(vendor).color(pal.accent).size(15.0).strong()); + }); + ui.vertical(|ui| { + ui.label(RichText::new(&i.cpu.name).color(pal.text).size(12.0).strong()); + ui.label( + RichText::new(i.cpu.socket.as_deref().unwrap_or("—")) + .color(pal.text_dim) + .size(10.5), + ); + }); + }); + ui.add_space(4.0); + + let cores = i + .cpu + .cores + .map(|c| format!("{c} / {}", i.cpu.threads.unwrap_or(c))) + .unwrap_or_default(); + info_row(ui, "Cores / Threads:", &cores, pal); + info_row( + ui, + "L2 Cache:", + &i.cpu.l2_kb.map(|k| format!("{} KB", k)).unwrap_or_default(), + pal, + ); + info_row( + ui, + "L3 Cache:", + &i.cpu.l3_kb.map(|k| format!("{} MB", k / 1024)).unwrap_or_default(), + pal, + ); + info_row(ui, "Codename:", &i.cpu.codename, pal); + info_row(ui, "CPUID:", &i.cpu.cpuid, pal); + info_row( + ui, + "Package Power:", + &cpu_sensor(tree, SensorType::Power, "package") + .map(|v| format!("{v:.1} W")) + .unwrap_or_default(), + pal, + ); + + ui.add_space(4.0); + ui.label(RichText::new("Features").color(pal.text_dim).size(10.5)); + // Fixed rows of 5 — deterministic wrap regardless of column width. + for row in i.cpu.features.chunks(5) { + ui.horizontal(|ui| { + for (name, on) in row { + chip(ui, name, *on, pal); + } + }); + } + + ui.add_space(6.0); + ui.label(RichText::new("Operating Point").color(pal.text_dim).size(10.5)); + operating_point_table(ui, i, tree, pal); + }); +} + +/// Min/Base/Boost/Avg clock table from WMI base clock + live core clocks/VIDs. +fn operating_point_table(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { + let clocks = collect_cpu(tree, SensorType::Clock, "core"); + let vids = collect_cpu(tree, SensorType::Voltage, "vid"); + let cur_avg = mean(&clocks); + let cur_max = clocks.iter().copied().fold(f32::NAN, f32::max); + let vid = mean(&vids); + + egui::Grid::new("op_table") + .num_columns(3) + .spacing([12.0, 2.0]) + .show(ui, |ui| { + let head = |ui: &mut egui::Ui, t: &str| { + ui.label(RichText::new(t).color(pal.text_dim).size(10.5).strong()); + }; + head(ui, ""); + head(ui, "Clock"); + head(ui, "VID"); + ui.end_row(); + + let row = |ui: &mut egui::Ui, name: &str, clock: Option, vid: Option, pal: &Palette| { + ui.label(RichText::new(name).color(pal.text).size(10.5)); + ui.label( + RichText::new(clock.map(|c| format!("{c:.1} MHz")).unwrap_or("—".into())) + .color(pal.clockc) + .size(10.5) + .monospace(), + ); + ui.label( + RichText::new(vid.map(|v| format!("{v:.4} V")).unwrap_or("—".into())) + .color(pal.volt) + .size(10.5) + .monospace(), + ); + ui.end_row(); + }; + row(ui, "Base Clock", i.cpu.base_clock_mhz.map(|c| c as f32), None, pal); + row(ui, "Max Clock", Some(cur_max).filter(|v| v.is_finite()), None, pal); + row(ui, "Avg. Active Clock", cur_avg, vid, pal); + }); +} + +fn board_memory_panels(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, pal: &Palette) { + panel(ui, "Motherboard", pal, |ui| { + ui.label( + RichText::new(format!("{} {}", i.board.manufacturer, i.board.product)) + .color(pal.text) + .size(12.0) + .strong(), + ); + ui.add_space(2.0); + info_row(ui, "Chipset:", "", pal); // needs PCI enum — native engine + info_row(ui, "BIOS Version:", &i.board.bios_version, pal); + info_row(ui, "BIOS Date:", &i.board.bios_date, pal); + }); + + ui.add_space(6.0); + + panel(ui, "Memory", pal, |ui| { + info_row( + ui, + "Size:", + &i.total_memory_gb.map(|g| format!("{g:.0} GB")).unwrap_or_default(), + pal, + ); + let mem_type = i + .memory_modules + .first() + .map(|m| crate::sysinfo::memory_type_label(&m.memory_type)) + .unwrap_or_default(); + info_row(ui, "Type:", &mem_type, pal); + let clock = i + .memory_modules + .first() + .and_then(|m| m.configured_speed_mts.or(m.speed_mts)) + .map(|v| format!("{v} MT/s")) + .unwrap_or_default(); + info_row(ui, "Clock:", &clock, pal); + // Unified memory is a wide on-package bus, not a DIMM channel count — + // inferring "Single-Channel" from the one synthetic module would be + // plainly wrong. + let unified = i + .memory_modules + .first() + .is_some_and(|m| m.memory_type.contains("on-package")); + let mode = if unified { + "Unified" + } else { + match i.memory_modules.len() { + 2 => "Dual-Channel", + 4 => "Quad-Channel", + 1 => "Single-Channel", + _ => "", + } + }; + info_row(ui, "Mode:", mode, pal); + info_row(ui, "Timings:", "", pal); // needs SPD — native engine + + ui.add_space(4.0); + ui.label(RichText::new("Memory Modules").color(pal.text_dim).size(10.5)); + for m in &i.memory_modules { + egui::Frame::new() + .fill(pal.bg_header) + .corner_radius(2) + .inner_margin(egui::Margin::same(4)) + .show(ui, |ui| { + ui.label( + RichText::new(format!("{}: {} {}", m.bank, m.manufacturer, m.part_number)) + .color(pal.text) + .size(10.5), + ); + ui.label( + RichText::new(format!( + "{:.0} GB {} @ {} MT/s {}", + m.capacity_gb, + m.memory_type, + m.configured_speed_mts.or(m.speed_mts).unwrap_or(0), + m.voltage_mv + .map(|v| format!("{:.2} V", v as f32 / 1000.0)) + .unwrap_or_default(), + )) + .color(pal.text_dim) + .size(10.0), + ); + }); + ui.add_space(2.0); + } + }); +} + +fn gpu_os_drives_panels(ui: &mut egui::Ui, i: &crate::sysinfo::SystemInfo, tree: &[Hardware], pal: &Palette) { + panel(ui, "GPU", pal, |ui| { + for (gi, g) in i.gpus.iter().enumerate() { + let vendor = if g.name.to_uppercase().contains("NVIDIA") { + "NVIDIA" + } else if g.name.to_uppercase().contains("AMD") || g.name.to_uppercase().contains("RADEON") { + "RADEON" + } else { + "GPU" + }; + ui.horizontal(|ui| { + egui::Frame::new() + .fill(pal.bg_header) + .corner_radius(3) + .inner_margin(egui::Margin::symmetric(8, 6)) + .show(ui, |ui| { + ui.label(RichText::new(vendor).color(pal.ok_badge).size(12.0).strong()); + }); + ui.vertical(|ui| { + ui.label(RichText::new(&g.name).color(pal.text).size(11.5).strong()); + // NOTE: WMI AdapterRAM is a u32 capped at 4 GB — showing it + // would be wrong for modern cards. VRAM comes with the + // native GPU engine (NVML/ADL). + ui.label( + RichText::new(format!("Driver {}", g.driver_version)) + .color(pal.text_dim) + .size(10.0), + ); + }); + }); + if gi + 1 < i.gpus.len() { + ui.add_space(3.0); + } + } + ui.add_space(4.0); + // Live GPU clocks from sensors. + let (core, mem) = gpu_live_clocks(tree); + info_row( + ui, + "GPU Clock:", + &core.map(|v| format!("{v:.1} MHz")).unwrap_or_default(), + pal, + ); + info_row( + ui, + "Memory Clock:", + &mem.map(|v| format!("{v:.1} MHz")).unwrap_or_default(), + pal, + ); + info_row(ui, "PCIe Link:", "", pal); // needs native engine + }); + + ui.add_space(6.0); + + panel(ui, "Operating System", pal, |ui| { + ui.label( + RichText::new(format!("{} ({})", i.os.caption, i.os.arch)) + .color(pal.text) + .size(11.0), + ); + info_row(ui, "Build:", &i.os.build, pal); + super::widgets::badge(ui, "UEFI Boot:", i.os.uefi_boot, pal); + super::widgets::badge(ui, "Secure Boot:", i.os.secure_boot, pal); + }); + + ui.add_space(6.0); + + panel(ui, "Drives", pal, |ui| { + for d in &i.drives { + ui.label( + RichText::new(format!( + "• {} [{}] {}", + d.model, + d.interface, + d.size_gb.map(|g| format!("{g:.0} GB")).unwrap_or_default() + )) + .color(pal.text) + .size(10.5), + ); + } + }); +} + +// ---- live-sensor helpers ------------------------------------------------ + +/// First CPU sensor of a type whose name contains `needle` (case-insensitive). +fn cpu_sensor(tree: &[Hardware], t: SensorType, needle: &str) -> Option { + for hw in tree { + if hw.hardware_type == HardwareType::Cpu { + for s in &hw.sensors { + if s.sensor_type == t && s.name.to_lowercase().contains(needle) { + return s.value; + } + } + } + } + None +} + +fn collect_cpu(tree: &[Hardware], t: SensorType, name_contains: &str) -> Vec { + let mut out = Vec::new(); + for hw in tree { + if hw.hardware_type == HardwareType::Cpu { + for s in &hw.sensors { + if s.sensor_type == t && s.name.to_lowercase().contains(name_contains) { + if let Some(v) = s.value { + out.push(v); + } + } + } + } + } + out +} + +fn gpu_live_clocks(tree: &[Hardware]) -> (Option, Option) { + let mut core = None; + let mut mem = None; + for hw in tree { + if matches!( + hw.hardware_type, + HardwareType::GpuNvidia + | HardwareType::GpuAti + | HardwareType::GpuIntel + | HardwareType::GpuApple + ) { + for s in &hw.sensors { + if s.sensor_type == SensorType::Clock { + let n = s.name.to_lowercase(); + if n.contains("core") && core.is_none() { + core = s.value; + } else if n.contains("memory") && mem.is_none() { + mem = s.value; + } + } + } + } + } + (core, mem) +} + +fn mean(v: &[f32]) -> Option { + if v.is_empty() { + None + } else { + Some(v.iter().sum::() / v.len() as f32) + } +} From fccf627b9ce96e588d8b2afe04e0a85f1320de65 Mon Sep 17 00:00:00 2001 From: Manupa Wickramasinghe <73810867+manupawickramasinghe@users.noreply.github.com> Date: Thu, 27 Aug 2026 03:36:54 +0530 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20Close=20the=20gap=20between?= =?UTF-8?q?=20what=20the=20memory-type=20table=20claims=20and=20decodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table skipped 0x08-0x0C (ROM, Flash, EEPROM, FEPROM, EPROM), so the claim that it decodes the DMTF range was not quite true — those fell to the unknown path. Added, and the doc comment now says explicitly that 0x15-0x17 are Reserved rather than memory types and are meant to fall through. A new test walks every code from 0x01 to 0x24 and asserts that only the Reserved three reach the unknown path, so the claim is checked rather than asserted. Co-Authored-By: Claude Opus 5 --- app/src/sysinfo.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/sysinfo.rs b/app/src/sysinfo.rs index 4363a696..4e07a818 100644 --- a/app/src/sysinfo.rs +++ b/app/src/sysinfo.rs @@ -101,7 +101,9 @@ fn codename_for(vendor: &str, family: u32, model: u32) -> String { /// /// Codes are the DMTF SMBIOS specification's, cross-checked against /// dmidecode's `dmi_memory_device_type` table, which runs from `0x01` to -/// `0x24`. +/// `0x24`. Every assigned code in that range is decoded; `0x15`–`0x17` are +/// Reserved rather than memory types, so they fall to the unknown path along +/// with anything DMTF has yet to assign. /// /// This used to decode exactly three values — DDR3, DDR4, DDR5 — and answer /// `"DRAM"` for everything else. The Summary appends " SDRAM" to whatever it @@ -121,6 +123,11 @@ fn smbios_memory_type(code: u32) -> String { 0x05 => "VRAM", 0x06 => "SRAM", 0x07 => "RAM", + 0x08 => "ROM", + 0x09 => "Flash", + 0x0A => "EEPROM", + 0x0B => "FEPROM", + 0x0C => "EPROM", 0x0D => "CDRAM", 0x0E => "3DRAM", 0x0F => "SDRAM", @@ -879,6 +886,23 @@ mod tests { assert_eq!(smbios_memory_type(0x24), "HBM3"); } + #[test] + fn every_assigned_code_from_1_to_0x24_decodes_to_a_name() { + // The claim this table makes: no assigned code falls through. 0x15-0x17 + // are Reserved rather than memory types, so they are the exception. + for code in 0x01..=0x24u32 { + let decoded = smbios_memory_type(code); + if (0x15..=0x17).contains(&code) { + assert_eq!(decoded, format!("Unknown (type {code:#04X})")); + } else { + assert!( + !decoded.starts_with("Unknown (type"), + "assigned code {code:#04X} fell through to the unknown path" + ); + } + } + } + #[test] fn an_unassigned_code_reports_the_raw_value_instead_of_guessing() { // 0x25 is past the last code DMTF has assigned. Showing the number is