From f03d7a94986dd5fb24e66281da06f4d1cb307b97 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Wed, 26 Aug 2026 21:45:45 -0400 Subject: [PATCH 01/30] Add Xeon example --- examples/xeon-e5-2470.txt | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 examples/xeon-e5-2470.txt diff --git a/examples/xeon-e5-2470.txt b/examples/xeon-e5-2470.txt new file mode 100644 index 00000000..1ed7a323 --- /dev/null +++ b/examples/xeon-e5-2470.txt @@ -0,0 +1,34 @@ +--------------- Rustid 2.0.0 (x86_64-linux) --------------- + System: PowerEdge R320 + + Architecture: x86_64-v2 + + Vendor: GenuineIntel (Intel) + + Model: Intel(R) Xeon(R) E5-2470 v2 + + MicroArch: Ivy Bridge + + Codename: Ivy Bridge-EP + + Process Node: 22nm + + Topology: 10 cores (20 threads) + + Cache: L1d: 10x 32 KB, 8-way + L1i: 10x 32 KB, 8-way + L2: 10x 256 KB, 8-way + L3: 25 MB, 20-way + + Frequency: 2.40 GHz + + Signature: Family 6h, Model 3Eh, Stepping 4h + (0, 6, 3, 14, 4) + + Features: Base: FPU TSC CX8 CX16 CMOV MMX HT APIC AMD64 + SSE: SSE SSE2 SSE3 SSE4.1 SSE4.2 SSSE3 + AVX: AVX + Security: NX RDRAND AES VT-x + Math: F16C + Other: x2apic POPCNT + From 172b973d66a49d02656e241ea9125cbdf97d9eb8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 16:17:46 -0400 Subject: [PATCH 02/30] Use the same logic for hybrid or homogenous x86 cpus --- src/common/mod.rs | 2 +- src/x86/cpu.rs | 111 ++++++++++++++++++++++++++++++--------- src/x86/display.rs | 77 ++++++++++++++++++--------- src/x86/mod.rs | 1 + tests/cpuid_dump_test.rs | 28 ++++++++++ 5 files changed, 168 insertions(+), 51 deletions(-) diff --git a/src/common/mod.rs b/src/common/mod.rs index ee4730bf..648f1cd6 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -105,7 +105,7 @@ impl From for CoreType { } /// CPU speed information (base and boost frequencies). -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] pub struct Speed { /// Base frequency in MHz pub base: u32, diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index 0606644a..b2465293 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -5,8 +5,7 @@ use super::micro_arch::{CpuArch, MicroArch}; use super::topology::Topology; use super::vendor::Cyrix; use super::*; -use super::{EXT_LEAF_2, EXT_LEAF_4, LEAF_1, read_multi_leaf_str, x86_cpuid}; -use crate::common::{Cache, CoreType, DataSource, TDetect, UNK}; +use crate::common::{Cache, CoreType, DataSource, Speed, TDetect, UNK}; use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; @@ -230,7 +229,7 @@ impl CpuSignature { } /// Information about a specific core type/cluster in the CPU. -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct CpuCore { /// Classification of this core (Performance, Efficiency, Super) pub kind: CoreType, @@ -240,6 +239,8 @@ pub struct CpuCore { pub name: Option<&'static str>, /// Cache hierarchy specific to this core type pub cache: Option, + /// Clock speed for this specific core type (base and boost frequencies) + pub speed: Option, /// Number of physical cores of this type pub count: u32, /// Number of logical threads of this type @@ -250,7 +251,7 @@ pub struct CpuCore { #[derive(Debug, Default, PartialEq)] pub struct Cpu { /// The system name, if applicable - #[cfg(any(not(nostd_os), target_os = "uefi"))] + #[cfg(not(dos_os))] pub system: Option, /// Does this cpu have cpuid instruction support pub has_cpuid: bool, @@ -552,9 +553,9 @@ impl TDetect for Cpu { /// Performs full CPU detection including architecture, microarchitecture, /// brand string, signature, features, and topology. fn detect() -> Self { - #[cfg(any(not(nostd_os), target_os = "uefi"))] + #[cfg(not(dos_os))] let system = { - #[cfg(not(nostd_os))] + #[cfg(not(target_os = "uefi"))] if provider::info_source() == provider::CpuidInfoSource::DumpFile { None } else { @@ -571,18 +572,23 @@ impl TDetect for Cpu { let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); let topology = Topology::detect(); - #[cfg(any(not(nostd_os), target_os = "uefi"))] + #[cfg(not(dos_os))] let cores = if is_intel() { - Self::detect_core_types() + let detected = Self::detect_core_types(); + if detected.len() > 1 { + detected + } else { + Self::fallback_homogeneous(&arch, &topology) + } } else { - Vec::new() + Self::fallback_homogeneous(&arch, &topology) }; - #[cfg(all(nostd_os, not(target_os = "uefi")))] - let cores = Vec::new(); + #[cfg(dos_os)] + let cores = Self::fallback_homogeneous(&arch, &topology); Self { - #[cfg(any(not(nostd_os), target_os = "uefi"))] + #[cfg(not(dos_os))] system, has_cpuid: (is_cyrix() && Cyrix::can_enable_cpuid()) || has_cpuid(), arch, @@ -601,7 +607,38 @@ impl TDetect for Cpu { } } -#[cfg(any(not(nostd_os), target_os = "uefi"))] +impl Cpu { + /// Creates a single homogeneous CpuCore cluster fallback based on the package topology and architecture. + pub fn fallback_homogeneous(arch: &CpuArch, topology: &Topology) -> Vec { + let speed = Speed::detect(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + let cache = topology.cache; + let sockets = topology.sockets.count.max(1); + let cores_per_socket = (topology.cores.count / sockets).max(1); + let threads_per_socket = (topology.threads.count / sockets).max(1); + let name = if arch.code_name != UNK { + Some(arch.code_name) + } else { + None + }; + alloc::vec![CpuCore { + kind: CoreType::Performance, + micro_arch: arch.micro_arch, + name, + cache, + speed: speed_opt, + count: cores_per_socket, + threads: threads_per_socket, + }] + } + + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } +} + +#[cfg(not(dos_os))] impl Cpu { /// Enumerates all logical processors to discover unique core types. /// @@ -619,22 +656,27 @@ impl Cpu { core_type: CoreType, name: Option<&'static str>, micro_arch: MicroArch, + speed: Option, cache: Option, count: u32, threads: u32, ) { if let Some(c) = cores .iter_mut() - .find(|c| c.kind == core_type && c.name == name) + .find(|c| c.kind == core_type && c.micro_arch == micro_arch && c.name == name) { c.count += count; c.threads += threads; + if c.speed.is_none() && speed.is_some() { + c.speed = speed; + } } else { cores.push(CpuCore { kind: core_type, micro_arch, name, cache, + speed, count, threads, }); @@ -644,7 +686,6 @@ impl Cpu { #[cfg(not(nostd_os))] if provider::info_source() == provider::CpuidInfoSource::DumpFile { let dump_count = provider::dump_cpu_count(); - let cache = Cache::detect(); for cpu_idx in 0..dump_count { provider::set_dump_cpu(cpu_idx); @@ -669,16 +710,20 @@ impl Cpu { None }; - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 1); - } + let cache = Cache::detect(); + let speed = Speed::detect(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; - return cores; + find_or_push( + &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + ); + } } #[cfg(not(nostd_os))] - if let Some(core_ids) = core_affinity::get_core_ids() { - let cache = Cache::detect(); - + if provider::info_source() != provider::CpuidInfoSource::DumpFile + && let Some(core_ids) = core_affinity::get_core_ids() + { for core_id in core_ids { core_affinity::set_for_current(core_id); @@ -703,26 +748,33 @@ impl Cpu { None }; - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 1); + let cache = Cache::detect(); + let speed = Speed::detect(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + + find_or_push( + &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + ); } } #[cfg(target_os = "uefi")] if let Some(mp) = crate::x86::efi::mp::EfiMpServices::detect() { let proc_count = mp.processor_count(); - let cache = Cache::detect(); for cpu_idx in 0..proc_count { let mut core_type = CoreType::default(); let mut sig = CpuSignature::default(); let mut raw_model = alloc::string::String::new(); let mut vendor = alloc::string::String::new(); + let mut speed = Speed::default(); mp.run_on_processor(cpu_idx, || { core_type = core_type_from_cpuid(); sig = CpuSignature::detect(); raw_model = Cpu::raw_model_string(); vendor = vendor_str(); + speed = Speed::detect(); }); let arch = CpuArch::find(&raw_model, sig, &vendor); @@ -743,11 +795,22 @@ impl Cpu { None }; - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 1); + let cache = Cache::detect(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + + find_or_push( + &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + ); } } for c in &mut cores { + let smt = if c.kind == CoreType::Efficiency { + 1 + } else { + cpuid_threads_per_core().max(1) + }; + c.count = (c.threads / smt).max(1); if let Some(ref mut cache) = c.cache { cache.resolve_share_counts(c.count, c.threads, 1); } diff --git a/src/x86/display.rs b/src/x86/display.rs index 2048e1dc..20bead6c 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -91,7 +91,7 @@ impl Cpu { } fn print_topology(&self, flags: CliFlags, disp: &CpuDisplay) { - if !self.cores.is_empty() { + if self.is_hybrid() { println!( "{}{} cores ({} threads) across {} core types", disp.label("Topology"), @@ -106,23 +106,42 @@ impl Cpu { println!("{}", disp.label(&core_label)); let type_str: &str = core.kind.into(); - println!("{}{}", disp.label("Type"), type_str); + disp.section_line("Type", type_str); if let Some(name) = &core.name { - println!("{}{}", disp.label("Codename"), name); + disp.section_line("Codename", name); } if core.count != core.threads { - println!( - "{}{} cores ({} threads)", - disp.label("Topology"), - core.count, - core.threads + disp.section_line( + "Topology", + &format!("{} cores ({} threads)", core.count, core.threads), ); } else { - println!("{}{} cores", disp.label("Topology"), core.count); + disp.section_line("Topology", &format!("{} cores", core.count)); } + if let Some(speed) = &core.speed + && speed.base > 0 { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + } else { + disp.section_line( + "Frequency", + &CpuDisplay::format_frequency(speed.base), + ); + } + } + let smt = cpuid_threads_per_core() .max(core.threads / core.count.max(1)) .max(1); @@ -177,9 +196,15 @@ impl Cpu { } fn print_speed(&self, disp: &CpuDisplay) { - if self.topology.speed.base > 0 { - let base = self.topology.speed.base; - let boost = self.topology.speed.boost; + let speed = self + .cores + .first() + .and_then(|c| c.speed.as_ref()) + .unwrap_or(&self.topology.speed); + + if speed.base > 0 { + let base = speed.base; + let boost = speed.boost; if boost > base { println!( @@ -193,11 +218,7 @@ impl Cpu { CpuDisplay::format_frequency(boost) ); } else { - println!( - "{}{}", - disp.label("Frequency"), - CpuDisplay::format_frequency(base) - ); + disp.section_line("Frequency", &CpuDisplay::format_frequency(base)); } disp.newline(); @@ -485,7 +506,7 @@ impl TCpuDisplay for Cpu { // Cache #[cfg(not(dos))] - if self.cores.is_empty() { + if !self.is_hybrid() { let cache_count = |share_count: u32| -> String { CpuDisplay::x86_cache_count( share_count, @@ -495,8 +516,14 @@ impl TCpuDisplay for Cpu { ) }; + let cache_opt = self + .cores + .first() + .and_then(|c| c.cache) + .or(self.topology.cache); + if is_asymmetric_dual_ccd_x3d(&self.display_model_string(), self.topology.dies.count) - && let Some(cache) = self.topology.cache + && let Some(cache) = cache_opt && let Some(l3) = cache.l3 { let x3d_mb = l3.size / (1024 * 1024); @@ -512,22 +539,20 @@ impl TCpuDisplay for Cpu { }; disp.display_cache_ext( - self.topology.cache, + cache_opt, &cache_count, self.topology.sockets.count, Some(&override_str), ); } else { - disp.display_cache( - self.topology.cache, - &cache_count, - self.topology.sockets.count, - ); + disp.display_cache(cache_opt, &cache_count, self.topology.sockets.count); } } // Clock Speed (Base/Boost) - self.print_speed(&disp); + if !self.is_hybrid() { + self.print_speed(&disp); + } // CPU Signature self.print_signature(flags, &disp); diff --git a/src/x86/mod.rs b/src/x86/mod.rs index 4aa89475..88710de9 100644 --- a/src/x86/mod.rs +++ b/src/x86/mod.rs @@ -45,5 +45,6 @@ pub use count::*; pub use cpu::*; pub use features::*; pub use fns::*; +pub use micro_arch::*; pub use quirks::*; diff --git a/tests/cpuid_dump_test.rs b/tests/cpuid_dump_test.rs index 9aa193c7..07d69263 100644 --- a/tests/cpuid_dump_test.rs +++ b/tests/cpuid_dump_test.rs @@ -462,6 +462,14 @@ cpuid_testsuite!( let cpu = Cpu::detect(); assert_cache_counts(&cpu, (4, "4x "), (4, "4x "), Some((4, "4x ")), Some((1, ""))); } + + test single_cluster_core { + let cpu = Cpu::detect(); + assert!(!cpu.is_hybrid()); + assert_eq!(cpu.cores.len(), 1); + assert_eq!(cpu.cores[0].kind, CoreType::Performance); + assert_eq!(cpu.cores[0].micro_arch, MicroArch::SandyBridge); + } } ); @@ -591,6 +599,14 @@ cpuid_testsuite!( assert!(!has_3dnow()); assert!(!has_3dnow_plus()); } + + test single_cluster_core { + let cpu = Cpu::detect(); + assert!(!cpu.is_hybrid()); + assert_eq!(cpu.cores.len(), 1); + assert_eq!(cpu.cores[0].kind, CoreType::Performance); + assert_eq!(cpu.cores[0].micro_arch, MicroArch::Zen3); + } } ); @@ -984,6 +1000,18 @@ cpuid_testsuite!( assert!(has_fma()); assert!(has_aes()); } + + test hybrid_cores { + let cpu = Cpu::detect(); + assert!(cpu.is_hybrid()); + assert_eq!(cpu.cores.len(), 2); + assert_eq!(cpu.cores[0].kind, CoreType::Performance); + assert_eq!(cpu.cores[0].micro_arch, MicroArch::GoldenCove); + assert_eq!(cpu.cores[0].name, Some("Golden Cove")); + assert_eq!(cpu.cores[1].kind, CoreType::Efficiency); + assert_eq!(cpu.cores[1].micro_arch, MicroArch::Gracemont); + assert_eq!(cpu.cores[1].name, Some("Gracemont")); + } } ); From 8dc28017bec0eae45da9097f68d4d808496a6e18 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 16:54:55 -0400 Subject: [PATCH 03/30] Refactor arm and riscv modules to better match x86 --- src/arm/cpu.rs | 19 +++++- src/arm/display.rs | 134 ++++++++++++++++++++++------------------ src/arm/micro_arch.rs | 4 +- src/arm/os/macos.rs | 4 +- src/arm/os/mod.rs | 25 +++++--- src/lib.rs | 2 +- src/riscv/cpu.rs | 21 ++++++- src/riscv/display.rs | 88 ++++++++++++++++++++++---- src/riscv/micro_arch.rs | 9 ++- src/riscv/mod.rs | 2 +- src/riscv/os/linux.rs | 78 +++++++++++++++++------ src/riscv/os/mod.rs | 2 +- src/x86/display.rs | 34 +++++----- 13 files changed, 292 insertions(+), 130 deletions(-) diff --git a/src/arm/cpu.rs b/src/arm/cpu.rs index 7e1adc49..ac2edd81 100644 --- a/src/arm/cpu.rs +++ b/src/arm/cpu.rs @@ -12,13 +12,30 @@ pub struct Cpu { pub model: String, pub system: Option, pub soc_model: Option, - pub cores: BTreeMap<(CoreType, Midr), CpuCore>, + pub cores: Vec, pub raw: BTreeMap, pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } +impl Cpu { + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } + + /// Total physical cores across all clusters + pub fn total_cores(&self) -> u32 { + self.cores.iter().map(|c| c.count).sum() + } + + /// Total logical threads across all clusters + pub fn total_threads(&self) -> u32 { + self.cores.iter().map(|c| c.threads).sum() + } +} + impl TDetect for Cpu { fn detect() -> Self { let info = crate::arm::os::detect(); diff --git a/src/arm/display.rs b/src/arm/display.rs index c71e3758..670c9561 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -30,7 +30,7 @@ impl CpuDisplay { if code_name != UNK && !code_name.is_empty() && is_duplicate(model, code_name) { return false; } - for core in cpu_info.cores.values() { + for core in &cpu_info.cores { let ma_str: String = core.micro_arch.into(); if ma_str != UNK && is_duplicate(model, &ma_str) { return false; @@ -59,7 +59,7 @@ impl CpuDisplay { // Check if every core cluster in cpu_info has the same code_name let mut common_cname: Option<&str> = None; - for (i, core) in cpu_info.cores.values().enumerate() { + for (i, core) in cpu_info.cores.iter().enumerate() { let Some(cname) = &core.code_name else { common_cname = None; break; @@ -67,7 +67,7 @@ impl CpuDisplay { if cname == UNK || cname.is_empty() { common_cname = None; break; - } + }; let ma_str: String = core.micro_arch.into(); if ma_str != UNK && is_duplicate(cname, &ma_str) { common_cname = None; @@ -122,7 +122,7 @@ impl CpuDisplay { // If this codename matches all core micro-architectures (e.g. Cortex-A53), suppress it let mut all_match_ma = !cpu_info.cores.is_empty(); - for core in cpu_info.cores.values() { + for core in &cpu_info.cores { let ma_str: String = core.micro_arch.into(); if ma_str == UNK || !is_duplicate(code_name, &ma_str) { all_match_ma = false; @@ -161,7 +161,7 @@ impl CpuDisplay { true } - pub fn display(cpu_info: &Cpu, flags: CliFlags) { + pub fn display_arm(cpu_info: &Cpu, flags: CliFlags) { let disp = CpuDisplay { flags }; disp.newline(); @@ -186,11 +186,9 @@ impl CpuDisplay { disp.simple_line("Process", tech); } - #[allow(clippy::explicit_counter_loop)] - if cpu_info.cores.len() > 1 { - let mut i = 1; - for core in cpu_info.cores.values() { - let core_num = format!("Core #{i}"); + if cpu_info.is_hybrid() { + for (i, core) in cpu_info.cores.iter().enumerate() { + let core_num = format!("Core #{}", i + 1); println!("{}", disp.label(&core_num)); let vendor_str: &str = core.implementer.into(); @@ -214,16 +212,33 @@ impl CpuDisplay { disp.section_line("Count", &core.count.to_string()); + if let Some(speed) = &core.speed + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + } else { + disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); + } + } + let cc = |s| CpuDisplay::cache_count(s, core.count); disp.display_cache(core.cache, &cc, 0); if core.cache.is_none() { disp.newline(); } - - i += 1; } - } else if let Some(core) = cpu_info.cores.values().next() { + } else if let Some(core) = cpu_info.cores.first() { println!("{}", disp.label("Cores")); let vendor_str: &str = core.implementer.into(); @@ -244,6 +259,25 @@ impl CpuDisplay { disp.section_line("Count", &core.count.to_string()); + if let Some(speed) = &core.speed + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + } else { + disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); + } + } + let cc = |s| CpuDisplay::cache_count(s, core.count); disp.display_cache(core.cache, &cc, 0); } @@ -292,7 +326,7 @@ impl TCpuDisplay for Cpu { } fn display_table(&self, flags: CliFlags) { - CpuDisplay::display(self, flags); + CpuDisplay::display_arm(self, flags); } } @@ -302,28 +336,25 @@ mod tests { use crate::arm::brand::Vendor; use crate::arm::micro_arch::CpuArch; use crate::common::CoreType; - use std::collections::{BTreeMap, HashSet}; - + use std::collections::HashSet; fn make_test_cpu( model: &str, code_name: &'static str, core_info: &[(Vendor, MicroArch, Option<&str>)], ) -> Cpu { - let mut cores = BTreeMap::new(); - for (i, &(implementer, ma, cname)) in core_info.iter().enumerate() { - let midr = Midr::new(i); + let mut cores = Vec::new(); + for &(implementer, ma, cname) in core_info { let kind = ma.core_type(); - cores.insert( - (kind, midr), - CpuCore { - implementer, - kind, - micro_arch: ma, - code_name: cname.map(String::from), - cache: None, - count: 4, - }, - ); + cores.push(CpuCore { + implementer, + kind, + micro_arch: ma, + code_name: cname.map(String::from), + cache: None, + speed: None, + count: 4, + threads: 4, + }); } Cpu { @@ -498,14 +529,8 @@ mod tests { ), ], ); - let core_gold = cpu_snapdragon - .cores - .get(&(CoreType::Performance, Midr::new(0))) - .expect("gold core missing"); - let core_silver = cpu_snapdragon - .cores - .get(&(CoreType::Efficiency, Midr::new(1))) - .expect("silver core missing"); + let core_gold = &cpu_snapdragon.cores[0]; + let core_silver = &cpu_snapdragon.cores[1]; // Since core types have different codenames, each core should show its own codename assert!(CpuDisplay::should_show_core_codename( @@ -525,10 +550,7 @@ mod tests { "Maya", &[(Vendor::Arm, MicroArch::ArmCortexA72, Some("Maya"))], ); - let core_a72 = cpu_a72 - .cores - .get(&(CoreType::Performance, Midr::new(0))) - .expect("a72 core missing"); + let core_a72 = &cpu_a72.cores[0]; // When all core types share the same codename, it's displayed only in the CPU/SoC section, NOT with the cores assert!(!CpuDisplay::should_show_core_codename( @@ -544,10 +566,7 @@ mod tests { "Cortex-A53", &[(Vendor::Arm, MicroArch::ArmCortexA53, Some("Cortex-A53"))], ); - let core_a53 = cpu_a53 - .cores - .get(&(CoreType::Efficiency, Midr::new(0))) - .expect("a53 core missing"); + let core_a53 = &cpu_a53.cores[0]; assert!(!CpuDisplay::should_show_core_codename( core_a53, &cpu_a53, false )); @@ -558,10 +577,7 @@ mod tests { "Tonga", &[(Vendor::Apple, MicroArch::AppleFirestorm, None)], ); - let core_apple = cpu_apple - .cores - .get(&(CoreType::Performance, Midr::new(0))) - .expect("apple core missing"); + let core_apple = &cpu_apple.cores[0]; assert!(!CpuDisplay::should_show_core_codename( core_apple, &cpu_apple, false )); @@ -579,7 +595,9 @@ mod tests { micro_arch: MicroArch::NvidiaDenver2, code_name: Some("Denver 2".to_string()), cache: None, + speed: None, count: 2, + threads: 2, }; let a57 = CpuCore { implementer: Vendor::Arm, @@ -587,7 +605,9 @@ mod tests { micro_arch: MicroArch::ArmCortexA57, code_name: Some("Cortex-A57".to_string()), cache: None, + speed: None, count: 4, + threads: 4, }; assert_eq!(denver.implementer, Vendor::Nvidia); @@ -607,14 +627,8 @@ mod tests { (Vendor::Arm, MicroArch::ArmCortexA55, Some("Cortex-A55")), ], ); - let gold = cpu_snapdragon - .cores - .get(&(CoreType::Performance, Midr::new(0))) - .expect("gold core missing"); - let silver = cpu_snapdragon - .cores - .get(&(CoreType::Efficiency, Midr::new(1))) - .expect("silver core missing"); + let gold = &cpu_snapdragon.cores[0]; + let silver = &cpu_snapdragon.cores[1]; assert_eq!(gold.implementer, Vendor::Qualcomm); assert_eq!(silver.implementer, Vendor::Arm); @@ -642,7 +656,7 @@ mod tests { compact: false, verbose: false, }; - CpuDisplay::display(&cpu, flags); + CpuDisplay::display_arm(&cpu, flags); } #[test] @@ -660,7 +674,7 @@ mod tests { compact: false, verbose: false, }; - CpuDisplay::display(&cpu, flags); + CpuDisplay::display_arm(&cpu, flags); } #[test] diff --git a/src/arm/micro_arch.rs b/src/arm/micro_arch.rs index e80779bf..bdb48280 100644 --- a/src/arm/micro_arch.rs +++ b/src/arm/micro_arch.rs @@ -1,7 +1,7 @@ use crate::arm::brand::*; use crate::common::CoreType; use crate::common::constants::*; -use crate::common::{Cache, UNK}; +use crate::common::{Cache, Speed, UNK}; pub const IMPLEMENTER_MASK: usize = 0xFF000000; pub const VARIANT_MASK: usize = 0x00F00000; @@ -55,7 +55,9 @@ pub struct CpuCore { pub micro_arch: MicroArch, pub code_name: Option, pub cache: Option, + pub speed: Option, pub count: u32, + pub threads: u32, } /// ARM Microarchitectures across vendors. diff --git a/src/arm/os/macos.rs b/src/arm/os/macos.rs index a7962d00..808a55ae 100644 --- a/src/arm/os/macos.rs +++ b/src/arm/os/macos.rs @@ -422,7 +422,9 @@ pub fn detect() -> OsCpuInfo { micro_arch, code_name: None, cache: Some(cache), + speed: None, count, + threads: count, }, ); } @@ -436,7 +438,7 @@ pub fn detect() -> OsCpuInfo { midrs, vendor, cpu_arch, - cores, + cores: cores.into_values().collect(), model, raw: values, midr_source: DataSource::Sysctrl("hw.cpufamily"), diff --git a/src/arm/os/mod.rs b/src/arm/os/mod.rs index 4c3eae45..f617602e 100644 --- a/src/arm/os/mod.rs +++ b/src/arm/os/mod.rs @@ -7,7 +7,7 @@ pub struct OsCpuInfo { pub midrs: HashSet, pub vendor: String, pub cpu_arch: CpuArch, - pub cores: BTreeMap<(CoreType, Midr), CpuCore>, + pub cores: Vec, pub model: String, pub raw: BTreeMap, pub midr_source: DataSource, @@ -18,7 +18,7 @@ pub struct OsCpuInfo { /// Iterates over MIDRs, assigning core types/names via `CpuArch::find()` /// and merging cache data from the runtime or sysfs. #[cfg(any(not(target_os = "macos"), test))] -pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore> { +pub(crate) fn detect_cores(midrs: &[Midr]) -> Vec { let mut cores: BTreeMap<(CoreType, Midr), CpuCore> = BTreeMap::new(); let runtime_cache = Cache::detect(); @@ -61,14 +61,19 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore cores .entry((core_type, *midr)) - .and_modify(|c| c.count += 1) + .and_modify(|c| { + c.count += 1; + c.threads += 1; + }) .or_insert(CpuCore { implementer, kind: core_type, micro_arch, code_name, cache, + speed: None, count: 1, + threads: 1, }); } @@ -78,7 +83,7 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore } } - cores + cores.into_values().collect() } // ---------------------------------------------------------------------------- @@ -167,7 +172,8 @@ mod tests { assert_eq!(cores.len(), 2); let silver_core = cores - .get(&(CoreType::Efficiency, silver_midr)) + .iter() + .find(|c| c.kind == CoreType::Efficiency) .expect("silver core missing"); assert_eq!(silver_core.count, 6); assert_eq!(silver_core.kind, CoreType::Efficiency); @@ -175,7 +181,8 @@ mod tests { assert_eq!(silver_core.micro_arch, MicroArch::ArmCortexA55); let gold_core = cores - .get(&(CoreType::Performance, gold_midr)) + .iter() + .find(|c| c.kind == CoreType::Performance) .expect("gold core missing"); assert_eq!(gold_core.count, 2); assert_eq!(gold_core.kind, CoreType::Performance); @@ -218,13 +225,15 @@ mod tests { assert_eq!(cores.len(), 2); let silver = cores - .get(&(CoreType::Efficiency, silver_midr)) + .iter() + .find(|c| c.kind == CoreType::Efficiency) .expect("silver core missing"); assert_eq!(silver.count, 4); assert_eq!(silver.implementer, Vendor::Qualcomm); let gold = cores - .get(&(CoreType::Performance, gold_midr)) + .iter() + .find(|c| c.kind == CoreType::Performance) .expect("gold core missing"); assert_eq!(gold.count, 4); assert_eq!(gold.implementer, Vendor::Qualcomm); diff --git a/src/lib.rs b/src/lib.rs index 3a33997d..ce06154a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ pub mod arm; #[cfg(arm_cpu)] pub use arm::Cpu; -#[cfg(target_arch = "riscv64")] +#[cfg(any(target_arch = "riscv64", test))] pub mod riscv; #[cfg(target_arch = "riscv64")] pub use riscv::Cpu; diff --git a/src/riscv/cpu.rs b/src/riscv/cpu.rs index 3eea99d8..51c7b2c9 100644 --- a/src/riscv/cpu.rs +++ b/src/riscv/cpu.rs @@ -10,13 +10,30 @@ pub struct Cpu { pub model: String, pub system: Option, pub isa_string: String, - pub cores: BTreeMap, + pub cores: Vec, pub raw: BTreeMap, pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } +impl Cpu { + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } + + /// Total physical cores across all clusters + pub fn total_cores(&self) -> u32 { + self.cores.iter().map(|c| c.count).sum() + } + + /// Total logical threads across all clusters + pub fn total_threads(&self) -> u32 { + self.cores.iter().map(|c| c.threads).sum() + } +} + impl TDetect for Cpu { fn detect() -> Self { let info = crate::riscv::os::detect(); @@ -46,6 +63,6 @@ impl TCpuDisplay for Cpu { } fn display_table(&self, flags: CliFlags) { - CpuDisplay::display(self, flags); + CpuDisplay::display_riscv(self, flags); } } diff --git a/src/riscv/display.rs b/src/riscv/display.rs index 7eb37c24..8cf08cf9 100644 --- a/src/riscv/display.rs +++ b/src/riscv/display.rs @@ -3,7 +3,7 @@ use crate::common::{CliFlags, CpuDisplay, UNK}; use crate::riscv::brand::format_uarch; impl CpuDisplay { - pub fn display(cpu_info: &Cpu, flags: CliFlags) { + pub fn display_riscv(cpu_info: &Cpu, flags: CliFlags) { let disp = CpuDisplay { flags }; disp.newline(); @@ -36,20 +36,82 @@ impl CpuDisplay { disp.simple_line("Process Node", tech); } - // Display topology - if !cpu_info.cores.is_empty() { - let total_cores: u32 = cpu_info.cores.values().map(|c| c.count).sum(); - println!("{}{} cores", disp.label("Topology"), total_cores); - disp.newline(); - } + // Display topology & per-core details + if cpu_info.is_hybrid() { + disp.simple_line( + "Topology", + &format!( + "{} cores across {} core types", + cpu_info.total_cores(), + cpu_info.cores.len() + ), + ); + + for (i, core) in cpu_info.cores.iter().enumerate() { + let core_label = format!("Core #{}", i + 1); + println!("{}", disp.label(&core_label)); + + let type_str: &str = core.kind.into(); + disp.section_line("Type", type_str); + + if let Some(name) = &core.name { + disp.section_line("MicroArch", name); + } + + disp.section_line("Count", &core.count.to_string()); + + if let Some(speed) = &core.speed + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + } else { + disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); + } + } + + let cc = |s| CpuDisplay::cache_count(s, core.count); + disp.display_cache(core.cache, &cc, 0); + + if core.cache.is_none() { + disp.newline(); + } + } + } else if let Some(core) = cpu_info.cores.first() { + disp.simple_line("Topology", &format!("{} cores", core.count)); - // Display cache at top level - if !cpu_info.cores.is_empty() { - let total_cores: u32 = cpu_info.cores.values().map(|c| c.count).sum(); - let first_core = cpu_info.cores.values().next().unwrap(); let cc = - |share_count: u32| -> String { CpuDisplay::cache_count(share_count, total_cores) }; - disp.display_cache(first_core.cache, &cc, 0); + |share_count: u32| -> String { CpuDisplay::cache_count(share_count, core.count) }; + disp.display_cache(core.cache, &cc, 0); + + if let Some(speed) = &core.speed + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + disp.newline(); + } else { + disp.simple_line("Frequency", &CpuDisplay::format_frequency(speed.base)); + } + } } // Display features diff --git a/src/riscv/micro_arch.rs b/src/riscv/micro_arch.rs index 9f48aacb..913d80f4 100644 --- a/src/riscv/micro_arch.rs +++ b/src/riscv/micro_arch.rs @@ -5,15 +5,18 @@ use crate::common::CoreType; use crate::common::constants::*; -use crate::common::{Cache, UNK}; +use crate::common::{Cache, Speed, UNK}; use crate::riscv::brand::*; #[derive(Debug, Clone, PartialEq)] pub struct CpuCore { pub kind: CoreType, + pub micro_arch: MicroArch, pub name: Option, pub cache: Option, + pub speed: Option, pub count: u32, + pub threads: u32, } /// RISC-V `misa` register layout. @@ -43,7 +46,7 @@ impl Misa { /// Returns true if the given single-letter extension is present. pub fn has_ext(&self, ch: char) -> bool { let c = ch.to_ascii_uppercase(); - if c < 'A' || c > 'Z' { + if !c.is_ascii_uppercase() { return false; } let bit = (c as u64) - ('A' as u64); @@ -509,7 +512,7 @@ mod tests { #[test] fn test_sifive_u74_find() { - let cpu = CpuArch::find(VENDOR_SIFIVE, 0x0000_0001); + let cpu = CpuArch::find(VENDOR_SIFIVE, 0x0000_0007); assert_eq!(cpu.model.as_str(), "SiFive U74"); assert_eq!(cpu.micro_arch, MicroArch::SiFiveU74); } diff --git a/src/riscv/mod.rs b/src/riscv/mod.rs index 74c41352..e4c84899 100644 --- a/src/riscv/mod.rs +++ b/src/riscv/mod.rs @@ -1,4 +1,4 @@ -#![cfg(target_arch = "riscv64")] +#![cfg(any(target_arch = "riscv64", test))] //! RISC-V CPU detection. pub mod brand; diff --git a/src/riscv/os/linux.rs b/src/riscv/os/linux.rs index e004a13c..f44d1007 100644 --- a/src/riscv/os/linux.rs +++ b/src/riscv/os/linux.rs @@ -4,7 +4,7 @@ use super::OsCpuInfo; use crate::common::{ - Cache, CoreType, DataSource, UNK, get_devicetree_compatible, get_proc_cpuinfo_data, + Cache, CoreType, DataSource, Speed, UNK, get_devicetree_compatible, get_proc_cpuinfo_data, }; use crate::riscv::brand::{Vendor, format_uarch}; use crate::riscv::micro_arch::*; @@ -76,7 +76,7 @@ pub fn detect() -> OsCpuInfo { let v: String = Vendor::from(mvendorid).into(); v } else { - let v: String = Vendor::from(mvendorid).into(); + let v: String = Vendor::Unknown.into(); v }; @@ -115,12 +115,11 @@ pub fn detect() -> OsCpuInfo { } // Fallback: if CSR-based identification yielded Unknown, try device tree compatible - if cpu_arch.model == UNK { - if let Some(ref compat) = dt_compat { - if let Some(dt_arch) = CpuArch::find_by_compatible(compat) { - cpu_arch = dt_arch; - } - } + if cpu_arch.model == UNK + && let Some(ref compat) = dt_compat + && let Some(dt_arch) = CpuArch::find_by_compatible(compat) + { + cpu_arch = dt_arch; } // Fallback: if ISA string is missing from /proc/cpuinfo, try device tree @@ -137,20 +136,19 @@ pub fn detect() -> OsCpuInfo { if let Some(c) = &mut cache { c.resolve_share_counts(core_count, core_count, 1); } + let speed = detect_speed(); let cores = if core_count > 0 { - let mut map = BTreeMap::new(); - map.insert( - core_type, - CpuCore { - kind: core_type, - name: Some(String::from(cpu_arch.micro_arch)), - cache, - count: core_count, - }, - ); - map + vec![CpuCore { + kind: core_type, + micro_arch: cpu_arch.micro_arch, + name: Some(String::from(cpu_arch.micro_arch)), + cache, + speed, + count: core_count, + threads: core_count, + }] } else { - BTreeMap::new() + vec![] }; OsCpuInfo { @@ -219,6 +217,46 @@ fn read_dt_cpu_freq() -> Option { } } +/// Read clock frequency from the device tree CPU node as a Speed struct. +fn read_dt_cpu_speed() -> Option { + let path = "/sys/firmware/devicetree/base/cpus/cpu@0/clock-frequency"; + let bytes = std::fs::read(path).ok()?; + if bytes.len() < 4 { + return None; + } + let freq_hz = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + if freq_hz == 0 { + return None; + } + let freq_mhz = freq_hz / 1_000_000; + Some(Speed { + base: freq_mhz, + boost: freq_mhz, + measured: false, + }) +} + +/// Detect CPU speed via device tree or sysfs cpufreq. +fn detect_speed() -> Option { + if let Some(speed) = read_dt_cpu_speed() { + return Some(speed); + } + let path = "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"; + if let Ok(content) = std::fs::read_to_string(path) + && let Ok(khz) = content.trim().parse::() + { + let mhz = khz / 1000; + if mhz > 0 { + return Some(Speed { + base: mhz, + boost: mhz, + measured: false, + }); + } + } + None +} + // ---------------------------------------------------------------------------- // Feature detection via /proc/cpuinfo // ---------------------------------------------------------------------------- diff --git a/src/riscv/os/mod.rs b/src/riscv/os/mod.rs index 5ca7b8a8..17c5dfad 100644 --- a/src/riscv/os/mod.rs +++ b/src/riscv/os/mod.rs @@ -8,7 +8,7 @@ pub struct OsCpuInfo { pub cpu_arch: CpuArch, pub model: String, pub isa_string: String, - pub cores: BTreeMap, + pub cores: Vec, pub raw: BTreeMap, pub midr_source: DataSource, pub features_source: DataSource, diff --git a/src/x86/display.rs b/src/x86/display.rs index 20bead6c..2f398e49 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -122,25 +122,23 @@ impl Cpu { } if let Some(speed) = &core.speed - && speed.base > 0 { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - } else { - disp.section_line( - "Frequency", - &CpuDisplay::format_frequency(speed.base), - ); - } + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + } else { + disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); } + } let smt = cpuid_threads_per_core() .max(core.threads / core.count.max(1)) From cb8629275f2837e60e72e913ec9d43e46148ae9f Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 17:09:20 -0400 Subject: [PATCH 04/30] Refactor PowerPC to match structure of other architectures --- src/lib.rs | 2 +- src/ppc/cpu.rs | 92 +++++++++++++++++++++++++++++++++---------- src/ppc/display.rs | 44 ++++++++++++++++----- src/ppc/micro_arch.rs | 53 +++++++++++++++++++++++++ src/ppc/mod.rs | 10 +++-- 5 files changed, 166 insertions(+), 35 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ce06154a..72027498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ pub mod x86; #[cfg(x86_cpu)] pub use x86::Cpu; -#[cfg(ppc_cpu)] +#[cfg(any(ppc_cpu, test))] pub mod ppc; #[cfg(ppc_cpu)] pub use ppc::cpu::Cpu; diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index 8f83388d..900c8d0a 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -4,8 +4,8 @@ use crate::common::cache::Cache; #[cfg(target_os = "linux")] use crate::common::get_proc_cpuinfo_data; use crate::common::os::TOSData; -use crate::common::{DataSource, TDetect}; -use crate::ppc::micro_arch::CpuArch; +use crate::common::{CoreType, DataSource, Speed, TDetect, UNK}; +use crate::ppc::micro_arch::{CpuArch, CpuCore}; use std::fs; use std::path::Path; @@ -16,8 +16,7 @@ pub struct Cpu { pub version: u16, pub revision: u16, pub cpu_arch: CpuArch, - pub cache: Option, - pub clock_speed: Option, + pub cores: Vec, pub clock_speed_source: DataSource, } @@ -28,6 +27,43 @@ impl Default for Cpu { } impl Cpu { + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } + + /// Total physical cores across all clusters + pub fn total_cores(&self) -> u32 { + self.cores.iter().map(|c| c.count).sum() + } + + /// Total logical threads across all clusters + pub fn total_threads(&self) -> u32 { + self.cores.iter().map(|c| c.threads).sum() + } + + fn detect_topology() -> (u32, u32) { + #[cfg(target_os = "linux")] + { + let cpuinfo = get_proc_cpuinfo_data(); + let thread_count = cpuinfo.len().max(1) as u32; + + // Check sysfs for SMT thread siblings per core + let path = "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list"; + if let Ok(content) = fs::read_to_string(path) { + let threads_per_core = crate::common::expand_cpu_list(&content).len().max(1) as u32; + let core_count = (thread_count / threads_per_core).max(1); + return (core_count, thread_count); + } + + (thread_count, thread_count) + } + #[cfg(not(target_os = "linux"))] + { + (1, 1) + } + } + fn detect_cache() -> Option { #[cfg(any(target_os = "linux", target_family = "unix"))] if let Some(cache) = Cache::detect() { @@ -66,17 +102,15 @@ impl Cpu { return None; } - if let Ok(freq_str) = fs::read_to_string(dt_root.join("clock-frequency")) { - if let Ok(freq_hz) = freq_str.trim().parse::() { + if let Ok(freq_str) = fs::read_to_string(dt_root.join("clock-frequency")) + && let Ok(freq_hz) = freq_str.trim().parse::() { return Some(freq_hz / 1_000_000); } - } - if let Ok(freq_str) = fs::read_to_string(dt_root.join("timebase-frequency")) { - if let Ok(freq_hz) = freq_str.trim().parse::() { + if let Ok(freq_str) = fs::read_to_string(dt_root.join("timebase-frequency")) + && let Ok(freq_hz) = freq_str.trim().parse::() { return Some(freq_hz / 1_000_000); } - } None } @@ -93,13 +127,11 @@ impl Cpu { }; for line in output_str.lines() { - if line.starts_with("CPU max MHz") || line.starts_with("CPU MHz") { - if let Some(value) = line.split(':').nth(1) { - if let Some(freq) = Self::parse_mhz_value(value) { + if (line.starts_with("CPU max MHz") || line.starts_with("CPU MHz")) + && let Some(value) = line.split(':').nth(1) + && let Some(freq) = Self::parse_mhz_value(value) { return Some(freq); } - } - } } None @@ -108,11 +140,10 @@ impl Cpu { fn detect_clock_speed_from_cpuinfo() -> Option { let cpuinfo = get_proc_cpuinfo_data(); for map in &cpuinfo { - if let Some(val) = map.get("cpu MHz").or_else(|| map.get("clock")) { - if let Some(freq) = Self::parse_mhz_value(val) { + if let Some(val) = map.get("cpu MHz").or_else(|| map.get("clock")) + && let Some(freq) = Self::parse_mhz_value(val) { return Some(freq); } - } } None @@ -144,11 +175,31 @@ impl TDetect for Cpu { let version = (pvr >> 16) as u16; let revision = (pvr & 0xFFFF) as u16; let cpu_arch = CpuArch::find(pvr); + let (core_count, thread_count) = Self::detect_topology(); let mut cache = Self::detect_cache(); if let Some(c) = &mut cache { - c.resolve_share_counts(1, 1, 1); + c.resolve_share_counts(core_count, thread_count, 1); } let (clock_speed, clock_speed_source) = Self::detect_clock_speed(); + let speed = clock_speed.map(|mhz| Speed { + base: mhz as u32, + boost: mhz as u32, + measured: false, + }); + + let cores = vec![CpuCore { + kind: CoreType::Performance, + micro_arch: cpu_arch.micro_arch, + name: if cpu_arch.marketing_name != UNK { + Some(cpu_arch.marketing_name.to_string()) + } else { + None + }, + cache, + speed, + count: core_count, + threads: thread_count, + }]; Self { system, @@ -156,8 +207,7 @@ impl TDetect for Cpu { version, revision, cpu_arch, - cache, - clock_speed, + cores, clock_speed_source, } } diff --git a/src/ppc/display.rs b/src/ppc/display.rs index 2dd9b60f..a93f1bc7 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -23,18 +23,42 @@ impl TCpuDisplay for Cpu { disp.simple_line("Process", tech); } - if let Some(clock_mhz) = self.clock_speed { - println!( - "{}{}", - disp.label("Frequency"), - CpuDisplay::format_frequency(clock_mhz) - ); - disp.newline(); + let total_cores = self.total_cores(); + let total_threads = self.total_threads(); + if total_cores > 0 { + if total_threads != total_cores { + disp.simple_line( + "Topology", + &alloc::format!("{} cores ({} threads)", total_cores, total_threads), + ); + } else { + disp.simple_line("Topology", &alloc::format!("{} cores", total_cores)); + } } - // TODO handle multiple cores/sockets - let cc = |s| CpuDisplay::cache_count(s, 1); - disp.display_cache(self.cache, &cc, 0); + if let Some(core) = self.cores.first() { + if let Some(speed) = &core.speed + && speed.base > 0 { + if speed.boost > speed.base { + println!( + "{}{}", + disp.inline_sublabel("Frequency", "Base"), + CpuDisplay::format_frequency(speed.base) + ); + println!( + "{}{}", + disp.sublabel("Boost"), + CpuDisplay::format_frequency(speed.boost) + ); + disp.newline(); + } else { + disp.simple_line("Frequency", &CpuDisplay::format_frequency(speed.base)); + } + } + + let cc = |s| CpuDisplay::cache_count(s, total_cores); + disp.display_cache(core.cache, &cc, 0); + } println!(); } diff --git a/src/ppc/micro_arch.rs b/src/ppc/micro_arch.rs index bf55e902..271ba57a 100644 --- a/src/ppc/micro_arch.rs +++ b/src/ppc/micro_arch.rs @@ -1,4 +1,23 @@ use crate::common::constants::*; +use crate::common::{Cache, CoreType, Speed}; + +#[derive(Debug, Clone, PartialEq)] +pub struct CpuCore { + /// Classification of this core (Performance, Efficiency, Super) + pub kind: CoreType, + /// Microarchitecture variant of this core + pub micro_arch: MicroArch, + /// Human-readable marketing / core codename (e.g. "PowerPC 750 (G3)") + pub name: Option, + /// Cache hierarchy for this core cluster + pub cache: Option, + /// Clock speed (base and boost frequencies in MHz) + pub speed: Option, + /// Physical core count + pub count: u32, + /// Logical thread count (e.g., taking SMT into account) + pub threads: u32, +} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MicroArch { @@ -306,4 +325,38 @@ mod tests { assert_eq!(cpu.marketing_name, "PowerPC 970"); assert_eq!(cpu.micro_arch, MicroArch::Ppc970); } + + #[test] + fn test_ppc_core_and_topology() { + use super::super::cpu::Cpu; + use crate::common::{CoreType, DataSource, Speed}; + + let core = CpuCore { + kind: CoreType::Performance, + micro_arch: MicroArch::Ppc970, + name: Some("PowerPC 970".to_string()), + cache: None, + speed: Some(Speed { + base: 2000, + boost: 2000, + measured: false, + }), + count: 2, + threads: 2, + }; + + let cpu = Cpu { + system: None, + pvr: 0x0039_0202, + version: 0x0039, + revision: 0x0202, + cpu_arch: CpuArch::find(0x0039_0202), + cores: vec![core], + clock_speed_source: DataSource::DefaultValue, + }; + + assert!(!cpu.is_hybrid()); + assert_eq!(cpu.total_cores(), 2); + assert_eq!(cpu.total_threads(), 2); + } } diff --git a/src/ppc/mod.rs b/src/ppc/mod.rs index 4b30821f..ca01d0fd 100644 --- a/src/ppc/mod.rs +++ b/src/ppc/mod.rs @@ -1,6 +1,6 @@ //! PowerPC CPU detection. -#[cfg(not(ppc_cpu))] +#[cfg(not(any(ppc_cpu, test)))] compile_error!("This crate only supports PowerPC architectures."); pub mod cpu; @@ -11,13 +11,17 @@ pub mod micro_arch; /// /// The PVR contains information about the CPU version and revision. pub fn get_pvr() -> u32 { - let mut pvr: u32 = 0; #[cfg(ppc_cpu)] { + let mut pvr: u32 = 0; // PVR is SPR 287 on classic PowerPC unsafe { core::arch::asm!("mfspr {pvr}, 287", pvr = out(reg) pvr, options(nomem, nostack)); } + pvr + } + #[cfg(not(ppc_cpu))] + { + 0 } - pvr } From c954f1871e151200f3fa0c11b44130c750036ae8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 17:58:48 -0400 Subject: [PATCH 05/30] Extract the common structure and display code for all cpu types --- src/arm/cpu.rs | 41 ++++------- src/arm/display.rs | 126 +++++++++++++------------------- src/arm/micro_arch.rs | 15 +--- src/arm/os/macos.rs | 4 +- src/arm/os/mod.rs | 22 ++++-- src/common/display.rs | 86 ++++++++++++++++++++++ src/common/mod.rs | 68 ++++++++++++++++++ src/ppc/cpu.rs | 78 ++++++++++---------- src/ppc/display.rs | 35 ++------- src/ppc/micro_arch.rs | 37 ++++------ src/riscv/cpu.rs | 41 ++++------- src/riscv/display.rs | 80 +++++---------------- src/riscv/micro_arch.rs | 13 +--- src/riscv/os/linux.rs | 1 + src/riscv/os/mod.rs | 1 + src/x86/cpu.rs | 152 +++++++++++++++++++-------------------- src/x86/display.rs | 55 ++++---------- src/x86/dump.rs | 4 +- tests/cpuid_dump_test.rs | 5 +- 19 files changed, 420 insertions(+), 444 deletions(-) diff --git a/src/arm/cpu.rs b/src/arm/cpu.rs index ac2edd81..8faa8b33 100644 --- a/src/arm/cpu.rs +++ b/src/arm/cpu.rs @@ -4,55 +4,40 @@ use super::*; use crate::common::*; use std::collections::{BTreeMap, HashSet}; +/// ARM architecture-specific data. #[derive(Debug, Default, PartialEq)] -pub struct Cpu { +pub struct ArmData { pub midrs: HashSet, - pub vendor: String, pub cpu_arch: CpuArch, - pub model: String, - pub system: Option, pub soc_model: Option, - pub cores: Vec, pub raw: BTreeMap, - pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } -impl Cpu { - /// Returns true if this CPU has multiple core types (hybrid architecture). - pub fn is_hybrid(&self) -> bool { - self.cores.len() > 1 - } - - /// Total physical cores across all clusters - pub fn total_cores(&self) -> u32 { - self.cores.iter().map(|c| c.count).sum() - } - - /// Total logical threads across all clusters - pub fn total_threads(&self) -> u32 { - self.cores.iter().map(|c| c.threads).sum() - } -} +pub type Cpu = crate::common::Cpu; impl TDetect for Cpu { fn detect() -> Self { let info = crate::arm::os::detect(); let features = super::get_all_features(); - Self { + let extra = ArmData { midrs: info.midrs, - vendor: info.vendor, cpu_arch: info.cpu_arch, - model: info.model, - system: OS::get_system_name(), soc_model: OS::get_soc(), - cores: info.cores, raw: info.raw, - features, midr_source: info.midr_source, features_source: info.features_source, + }; + + Self { + system: OS::get_system_name(), + vendor: info.vendor, + model: info.model, + cores: info.cores, + features, + extra, } } } diff --git a/src/arm/display.rs b/src/arm/display.rs index 670c9561..373d8564 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -35,7 +35,7 @@ impl CpuDisplay { if ma_str != UNK && is_duplicate(model, &ma_str) { return false; } - if let Some(cname) = &core.code_name + if let Some(cname) = &core.name && cname != UNK && !cname.is_empty() && is_duplicate(model, cname) @@ -60,7 +60,7 @@ impl CpuDisplay { // Check if every core cluster in cpu_info has the same code_name let mut common_cname: Option<&str> = None; for (i, core) in cpu_info.cores.iter().enumerate() { - let Some(cname) = &core.code_name else { + let Some(cname) = &core.name else { common_cname = None; break; }; @@ -141,7 +141,7 @@ impl CpuDisplay { } pub fn should_show_core_codename(core: &CpuCore, cpu_info: &Cpu, verbose: bool) -> bool { - let Some(code_name) = &core.code_name else { + let Some(code_name) = &core.name else { return false; }; if code_name == UNK || code_name.is_empty() { @@ -191,10 +191,10 @@ impl CpuDisplay { let core_num = format!("Core #{}", i + 1); println!("{}", disp.label(&core_num)); - let vendor_str: &str = core.implementer.into(); - if vendor_str != UNK { - disp.section_line("Implementer", vendor_str); - } + if let Some(ref vendor_str) = core.implementer + && vendor_str != UNK { + disp.section_line("Implementer", vendor_str); + } let name = Into::<&str>::into(core.kind); disp.section_line("Type", name); @@ -205,31 +205,20 @@ impl CpuDisplay { } if Self::should_show_core_codename(core, cpu_info, flags.verbose) - && let Some(codename) = &core.code_name + && let Some(codename) = &core.name { disp.section_line("Codename", codename); } disp.section_line("Count", &core.count.to_string()); - if let Some(speed) = &core.speed - && speed.base > 0 - { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - } else { - disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); let cc = |s| CpuDisplay::cache_count(s, core.count); disp.display_cache(core.cache, &cc, 0); @@ -241,10 +230,10 @@ impl CpuDisplay { } else if let Some(core) = cpu_info.cores.first() { println!("{}", disp.label("Cores")); - let vendor_str: &str = core.implementer.into(); - if vendor_str != UNK { - disp.section_line("Implementer", vendor_str); - } + if let Some(ref vendor_str) = core.implementer + && vendor_str != UNK { + disp.section_line("Implementer", vendor_str); + } let ma_str: String = core.micro_arch.into(); if Self::should_show_core_micro_arch(core.micro_arch, flags.verbose) { @@ -252,50 +241,24 @@ impl CpuDisplay { } if Self::should_show_core_codename(core, cpu_info, flags.verbose) - && let Some(codename) = &core.code_name + && let Some(codename) = &core.name { disp.section_line("Codename", codename); } disp.section_line("Count", &core.count.to_string()); - if let Some(speed) = &core.speed - && speed.base > 0 - { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - } else { - disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency(core.speed, flags); let cc = |s| CpuDisplay::cache_count(s, core.count); disp.display_cache(core.cache, &cc, 0); } // Display features - if !cpu_info.features.is_empty() { - let keys = ["Base", "SIMD", "Security", "Atomics", "Fp", "Misc"]; - for key in keys { - if let Some(feat_str) = cpu_info.features.get(key) { - if key == "Base" { - println!("{}{}", disp.inline_sublabel("Features", "Base"), feat_str); - } else { - println!("{}{}", disp.sublabel(key), feat_str); - } - } - } - println!(); - } + disp.display_features( + &cpu_info.features, + &["Base", "SIMD", "Security", "Atomics", "Fp", "Misc"], + ); } } @@ -345,11 +308,16 @@ mod tests { let mut cores = Vec::new(); for &(implementer, ma, cname) in core_info { let kind = ma.core_type(); + let impl_str = if implementer != Vendor::Unknown { + Some(Into::<&str>::into(implementer).to_string()) + } else { + None + }; cores.push(CpuCore { - implementer, kind, micro_arch: ma, - code_name: cname.map(String::from), + name: cname.map(String::from), + implementer: impl_str, cache: None, speed: None, count: 4, @@ -358,9 +326,12 @@ mod tests { } Cpu { - cpu_arch: CpuArch { - model: model.to_string(), - code_name, + extra: ArmData { + cpu_arch: CpuArch { + model: model.to_string(), + code_name, + ..Default::default() + }, ..Default::default() }, cores, @@ -590,28 +561,28 @@ mod tests { fn test_multi_implementer_cores() { // Tegra X2: Nvidia Denver 2 + ARM Cortex-A57 let denver = CpuCore { - implementer: Vendor::Nvidia, kind: CoreType::Performance, micro_arch: MicroArch::NvidiaDenver2, - code_name: Some("Denver 2".to_string()), + name: Some("Denver 2".to_string()), + implementer: Some("Nvidia".to_string()), cache: None, speed: None, count: 2, threads: 2, }; let a57 = CpuCore { - implementer: Vendor::Arm, kind: CoreType::Performance, micro_arch: MicroArch::ArmCortexA57, - code_name: Some("Cortex-A57".to_string()), + name: Some("Cortex-A57".to_string()), + implementer: Some("ARM".to_string()), cache: None, speed: None, count: 4, threads: 4, }; - assert_eq!(denver.implementer, Vendor::Nvidia); - assert_eq!(a57.implementer, Vendor::Arm); + assert_eq!(denver.implementer.as_deref(), Some("Nvidia")); + assert_eq!(a57.implementer.as_deref(), Some("ARM")); assert_ne!(denver.implementer, a57.implementer); // Snapdragon 855: Qualcomm Kryo 485 Gold (Cortex-A76) + ARM Cortex-A55 @@ -630,8 +601,8 @@ mod tests { let gold = &cpu_snapdragon.cores[0]; let silver = &cpu_snapdragon.cores[1]; - assert_eq!(gold.implementer, Vendor::Qualcomm); - assert_eq!(silver.implementer, Vendor::Arm); + assert_eq!(gold.implementer.as_deref(), Some("Qualcomm")); + assert_eq!(silver.implementer.as_deref(), Some("ARM")); assert!(CpuDisplay::should_show_core_codename( gold, &cpu_snapdragon, @@ -684,7 +655,10 @@ mod tests { midrs.insert(Midr::new(0x410FD070)); // ARM Cortex-A57 let cpu = Cpu { - midrs, + extra: ArmData { + midrs, + ..Default::default() + }, ..Default::default() }; cpu.debug(); diff --git a/src/arm/micro_arch.rs b/src/arm/micro_arch.rs index bdb48280..dfdc54c1 100644 --- a/src/arm/micro_arch.rs +++ b/src/arm/micro_arch.rs @@ -1,7 +1,6 @@ use crate::arm::brand::*; -use crate::common::CoreType; +use crate::common::UNK; use crate::common::constants::*; -use crate::common::{Cache, Speed, UNK}; pub const IMPLEMENTER_MASK: usize = 0xFF000000; pub const VARIANT_MASK: usize = 0x00F00000; @@ -48,17 +47,7 @@ impl Midr { pub type Implementer = Vendor; -#[derive(Debug, Clone, PartialEq)] -pub struct CpuCore { - pub implementer: Implementer, - pub kind: CoreType, - pub micro_arch: MicroArch, - pub code_name: Option, - pub cache: Option, - pub speed: Option, - pub count: u32, - pub threads: u32, -} +pub type CpuCore = crate::common::CpuCore; /// ARM Microarchitectures across vendors. /// diff --git a/src/arm/os/macos.rs b/src/arm/os/macos.rs index 808a55ae..3080c153 100644 --- a/src/arm/os/macos.rs +++ b/src/arm/os/macos.rs @@ -417,10 +417,10 @@ pub fn detect() -> OsCpuInfo { cores.insert( (kind, midr), CpuCore { - implementer: Vendor::Apple, kind, micro_arch, - code_name: None, + name: None, + implementer: Some(Into::<&str>::into(Vendor::Apple).to_string()), cache: Some(cache), speed: None, count, diff --git a/src/arm/os/mod.rs b/src/arm/os/mod.rs index f617602e..607fbb96 100644 --- a/src/arm/os/mod.rs +++ b/src/arm/os/mod.rs @@ -1,4 +1,6 @@ +use super::micro_arch::CpuCore; use super::micro_arch::*; +use crate::arm::brand::Vendor; use crate::common::*; use std::collections::{BTreeMap, HashSet}; @@ -59,6 +61,12 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> Vec { let cache = core_cache_map.get(&midr.to_bits()).cloned().flatten(); + let impl_str = if implementer != Vendor::Unknown { + Some(Into::<&str>::into(implementer).to_string()) + } else { + None + }; + cores .entry((core_type, *midr)) .and_modify(|c| { @@ -66,10 +74,10 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> Vec { c.threads += 1; }) .or_insert(CpuCore { - implementer, kind: core_type, micro_arch, - code_name, + name: code_name, + implementer: impl_str, cache, speed: None, count: 1, @@ -134,7 +142,7 @@ pub use bsd::*; #[cfg(test)] mod tests { use super::*; - use crate::arm::brand::{IMPL_ARM, IMPL_QUALCOMM, Vendor}; + use crate::arm::brand::{IMPL_ARM, IMPL_QUALCOMM}; #[test] fn test_snapdragon_750g_detect_cores() { @@ -177,7 +185,7 @@ mod tests { .expect("silver core missing"); assert_eq!(silver_core.count, 6); assert_eq!(silver_core.kind, CoreType::Efficiency); - assert_eq!(silver_core.implementer, Vendor::Arm); + assert_eq!(silver_core.implementer.as_deref(), Some("ARM")); assert_eq!(silver_core.micro_arch, MicroArch::ArmCortexA55); let gold_core = cores @@ -186,7 +194,7 @@ mod tests { .expect("gold core missing"); assert_eq!(gold_core.count, 2); assert_eq!(gold_core.kind, CoreType::Performance); - assert_eq!(gold_core.implementer, Vendor::Arm); + assert_eq!(gold_core.implementer.as_deref(), Some("ARM")); assert_eq!(gold_core.micro_arch, MicroArch::ArmCortexA77); } @@ -229,13 +237,13 @@ mod tests { .find(|c| c.kind == CoreType::Efficiency) .expect("silver core missing"); assert_eq!(silver.count, 4); - assert_eq!(silver.implementer, Vendor::Qualcomm); + assert_eq!(silver.implementer.as_deref(), Some("Qualcomm")); let gold = cores .iter() .find(|c| c.kind == CoreType::Performance) .expect("gold core missing"); assert_eq!(gold.count, 4); - assert_eq!(gold.implementer, Vendor::Qualcomm); + assert_eq!(gold.implementer.as_deref(), Some("Qualcomm")); } } diff --git a/src/common/display.rs b/src/common/display.rs index a80f2a77..2812a536 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -1,3 +1,4 @@ +use super::Speed; use super::cache::{Cache, Level1Cache}; use super::constants::*; @@ -98,6 +99,91 @@ impl CpuDisplay { } } + /// Displays frequency lines (Base/Boost inline sublabels or single section line). + pub fn display_frequency(&self, speed: Option, flags: CliFlags) { + if let Some(speed) = speed + && speed.base > 0 { + if speed.boost > speed.base { + println!( + "{}{}", + self.inline_sublabel("Frequency", "Base"), + Self::format_frequency(speed.base) + ); + println!( + "{}{}", + self.sublabel("Boost"), + Self::format_frequency(speed.boost) + ); + } else { + self.section_line("Frequency", &Self::format_frequency(speed.base)); + } + if !flags.compact { + self.newline(); + } + } + } + + /// Displays the Topology line for homogeneous or hybrid configurations. + pub fn display_topology_line( + &self, + total_cores: u32, + total_threads: u32, + is_hybrid: bool, + cluster_count: usize, + ) { + if is_hybrid { + if total_threads != total_cores { + self.simple_line( + "Topology", + &alloc::format!( + "{} cores ({} threads) across {} core types", + total_cores, + total_threads, + cluster_count + ), + ); + } else { + self.simple_line( + "Topology", + &alloc::format!("{} cores across {} core types", total_cores, cluster_count), + ); + } + } else if total_cores > 0 { + if total_threads != total_cores { + self.simple_line( + "Topology", + &alloc::format!("{} cores ({} threads)", total_cores, total_threads), + ); + } else { + self.simple_line("Topology", &alloc::format!("{} cores", total_cores)); + } + } + } + + /// Displays detected features using the standard ordered key list. + pub fn display_features( + &self, + features: &alloc::collections::BTreeMap, + keys: &[&str], + ) where + K: core::borrow::Borrow + Ord, + { + if !features.is_empty() { + let mut first = true; + for key in keys { + if let Some(feat_str) = features.get(*key) { + if first { + println!("{}{}", self.inline_sublabel("Features", key), feat_str); + first = false; + } else { + println!("{}{}", self.sublabel(key), feat_str); + } + } + } + println!(); + } + } + pub fn cache_count(share_count: u32, core_count: u32) -> String { if share_count == 0 || (core_count / share_count) <= 1 { String::new() diff --git a/src/common/mod.rs b/src/common/mod.rs index 648f1cd6..67728050 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -115,6 +115,74 @@ pub struct Speed { pub measured: bool, } +/// Information about a specific core type/cluster in the CPU. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct CpuCore { + /// Classification of this core (Performance, Efficiency, Super) + pub kind: CoreType, + /// Microarchitecture variant of this core type + pub micro_arch: M, + /// Marketing or core codename (e.g., "Golden Cove", "Cortex-A78", "U74") + pub name: Option, + /// Core implementer / designer (e.g., "ARM", "Nvidia", "Apple") + pub implementer: Option, + /// Cache hierarchy specific to this core cluster + pub cache: Option, + /// Clock speed for this specific core cluster (base and boost frequencies in MHz) + pub speed: Option, + /// Number of physical cores in this cluster + pub count: u32, + /// Number of logical threads in this cluster + pub threads: u32, +} + +/// Unified CPU representation across all hardware architectures. +#[derive(Debug, Default, PartialEq)] +pub struct Cpu { + /// The system name, if applicable + pub system: Option, + /// CPU vendor name + pub vendor: String, + /// CPU model name + pub model: String, + /// Per-core-cluster breakdown of CPU cores + pub cores: alloc::vec::Vec>, + /// Detected CPU features + pub features: alloc::collections::BTreeMap<&'static str, String>, + /// Architecture-specific extension data + pub extra: E, +} + +impl Cpu { + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } + + /// Total physical cores across all clusters + pub fn total_cores(&self) -> u32 { + self.cores.iter().map(|c| c.count).sum() + } + + /// Total logical threads across all clusters + pub fn total_threads(&self) -> u32 { + self.cores.iter().map(|c| c.threads).sum() + } +} + +impl core::ops::Deref for Cpu { + type Target = E; + fn deref(&self) -> &Self::Target { + &self.extra + } +} + +impl core::ops::DerefMut for Cpu { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.extra + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub struct TopologyTier { pub count: u32, diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index 900c8d0a..1e45fff9 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -5,43 +5,23 @@ use crate::common::cache::Cache; use crate::common::get_proc_cpuinfo_data; use crate::common::os::TOSData; use crate::common::{CoreType, DataSource, Speed, TDetect, UNK}; -use crate::ppc::micro_arch::{CpuArch, CpuCore}; +use crate::ppc::micro_arch::{CpuArch, CpuCore, MicroArch}; use std::fs; use std::path::Path; -#[derive(Debug, PartialEq)] -pub struct Cpu { - pub system: Option, +/// PowerPC architecture-specific data. +#[derive(Debug, Default, PartialEq)] +pub struct PpcData { pub pvr: u32, pub version: u16, pub revision: u16, pub cpu_arch: CpuArch, - pub cores: Vec, pub clock_speed_source: DataSource, } -impl Default for Cpu { - fn default() -> Self { - Self::detect() - } -} +pub type Cpu = crate::common::Cpu; impl Cpu { - /// Returns true if this CPU has multiple core types (hybrid architecture). - pub fn is_hybrid(&self) -> bool { - self.cores.len() > 1 - } - - /// Total physical cores across all clusters - pub fn total_cores(&self) -> u32 { - self.cores.iter().map(|c| c.count).sum() - } - - /// Total logical threads across all clusters - pub fn total_threads(&self) -> u32 { - self.cores.iter().map(|c| c.threads).sum() - } - fn detect_topology() -> (u32, u32) { #[cfg(target_os = "linux")] { @@ -103,14 +83,16 @@ impl Cpu { } if let Ok(freq_str) = fs::read_to_string(dt_root.join("clock-frequency")) - && let Ok(freq_hz) = freq_str.trim().parse::() { - return Some(freq_hz / 1_000_000); - } + && let Ok(freq_hz) = freq_str.trim().parse::() + { + return Some(freq_hz / 1_000_000); + } if let Ok(freq_str) = fs::read_to_string(dt_root.join("timebase-frequency")) - && let Ok(freq_hz) = freq_str.trim().parse::() { - return Some(freq_hz / 1_000_000); - } + && let Ok(freq_hz) = freq_str.trim().parse::() + { + return Some(freq_hz / 1_000_000); + } None } @@ -129,9 +111,10 @@ impl Cpu { for line in output_str.lines() { if (line.starts_with("CPU max MHz") || line.starts_with("CPU MHz")) && let Some(value) = line.split(':').nth(1) - && let Some(freq) = Self::parse_mhz_value(value) { - return Some(freq); - } + && let Some(freq) = Self::parse_mhz_value(value) + { + return Some(freq); + } } None @@ -141,9 +124,10 @@ impl Cpu { let cpuinfo = get_proc_cpuinfo_data(); for map in &cpuinfo { if let Some(val) = map.get("cpu MHz").or_else(|| map.get("clock")) - && let Some(freq) = Self::parse_mhz_value(val) { - return Some(freq); - } + && let Some(freq) = Self::parse_mhz_value(val) + { + return Some(freq); + } } None @@ -195,20 +179,34 @@ impl TDetect for Cpu { } else { None }, + implementer: None, cache, speed, count: core_count, threads: thread_count, }]; - Self { - system, + let extra = PpcData { pvr, version, revision, cpu_arch, - cores, clock_speed_source, + }; + + let vendor = String::from(if extra.cpu_arch.marketing_name != UNK { + "IBM" + } else { + UNK + }); + + Self { + system, + vendor, + model: extra.cpu_arch.marketing_name.to_string(), + cores, + features: std::collections::BTreeMap::new(), + extra, } } } diff --git a/src/ppc/display.rs b/src/ppc/display.rs index a93f1bc7..27a04e88 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -25,36 +25,15 @@ impl TCpuDisplay for Cpu { let total_cores = self.total_cores(); let total_threads = self.total_threads(); - if total_cores > 0 { - if total_threads != total_cores { - disp.simple_line( - "Topology", - &alloc::format!("{} cores ({} threads)", total_cores, total_threads), - ); - } else { - disp.simple_line("Topology", &alloc::format!("{} cores", total_cores)); - } - } + disp.display_topology_line( + total_cores, + total_threads, + self.is_hybrid(), + self.cores.len(), + ); if let Some(core) = self.cores.first() { - if let Some(speed) = &core.speed - && speed.base > 0 { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - disp.newline(); - } else { - disp.simple_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency(core.speed, flags); let cc = |s| CpuDisplay::cache_count(s, total_cores); disp.display_cache(core.cache, &cc, 0); diff --git a/src/ppc/micro_arch.rs b/src/ppc/micro_arch.rs index 271ba57a..0fd488ae 100644 --- a/src/ppc/micro_arch.rs +++ b/src/ppc/micro_arch.rs @@ -1,23 +1,6 @@ use crate::common::constants::*; -use crate::common::{Cache, CoreType, Speed}; -#[derive(Debug, Clone, PartialEq)] -pub struct CpuCore { - /// Classification of this core (Performance, Efficiency, Super) - pub kind: CoreType, - /// Microarchitecture variant of this core - pub micro_arch: MicroArch, - /// Human-readable marketing / core codename (e.g. "PowerPC 750 (G3)") - pub name: Option, - /// Cache hierarchy for this core cluster - pub cache: Option, - /// Clock speed (base and boost frequencies in MHz) - pub speed: Option, - /// Physical core count - pub count: u32, - /// Logical thread count (e.g., taking SMT into account) - pub threads: u32, -} +pub type CpuCore = crate::common::CpuCore; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MicroArch { @@ -328,13 +311,14 @@ mod tests { #[test] fn test_ppc_core_and_topology() { - use super::super::cpu::Cpu; + use super::super::cpu::{Cpu, PpcData}; use crate::common::{CoreType, DataSource, Speed}; let core = CpuCore { kind: CoreType::Performance, micro_arch: MicroArch::Ppc970, name: Some("PowerPC 970".to_string()), + implementer: None, cache: None, speed: Some(Speed { base: 2000, @@ -347,12 +331,17 @@ mod tests { let cpu = Cpu { system: None, - pvr: 0x0039_0202, - version: 0x0039, - revision: 0x0202, - cpu_arch: CpuArch::find(0x0039_0202), + vendor: String::from("IBM"), + model: String::from("PowerPC 970"), cores: vec![core], - clock_speed_source: DataSource::DefaultValue, + features: std::collections::BTreeMap::new(), + extra: PpcData { + pvr: 0x0039_0202, + version: 0x0039, + revision: 0x0202, + cpu_arch: CpuArch::find(0x0039_0202), + clock_speed_source: DataSource::DefaultValue, + }, }; assert!(!cpu.is_hybrid()); diff --git a/src/riscv/cpu.rs b/src/riscv/cpu.rs index 51c7b2c9..0c0f2ac4 100644 --- a/src/riscv/cpu.rs +++ b/src/riscv/cpu.rs @@ -3,53 +3,38 @@ use super::micro_arch::*; use crate::common::*; use std::collections::BTreeMap; +/// RISC-V architecture-specific data. #[derive(Debug, Default, PartialEq)] -pub struct Cpu { - pub vendor: String, +pub struct RiscvData { pub cpu_arch: CpuArch, - pub model: String, - pub system: Option, pub isa_string: String, - pub cores: Vec, pub raw: BTreeMap, - pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } -impl Cpu { - /// Returns true if this CPU has multiple core types (hybrid architecture). - pub fn is_hybrid(&self) -> bool { - self.cores.len() > 1 - } - - /// Total physical cores across all clusters - pub fn total_cores(&self) -> u32 { - self.cores.iter().map(|c| c.count).sum() - } - - /// Total logical threads across all clusters - pub fn total_threads(&self) -> u32 { - self.cores.iter().map(|c| c.threads).sum() - } -} +pub type Cpu = crate::common::Cpu; impl TDetect for Cpu { fn detect() -> Self { let info = crate::riscv::os::detect(); let features = super::get_all_features(&info.isa_string); - Self { - vendor: info.vendor, + let extra = RiscvData { cpu_arch: info.cpu_arch, - model: info.model, - system: OS::get_system_name(), isa_string: info.isa_string, - cores: info.cores, raw: info.raw, - features, midr_source: info.midr_source, features_source: info.features_source, + }; + + Self { + system: OS::get_system_name(), + vendor: info.vendor, + model: info.model, + cores: info.cores, + features, + extra, } } } diff --git a/src/riscv/display.rs b/src/riscv/display.rs index 8cf08cf9..2494301c 100644 --- a/src/riscv/display.rs +++ b/src/riscv/display.rs @@ -38,13 +38,11 @@ impl CpuDisplay { // Display topology & per-core details if cpu_info.is_hybrid() { - disp.simple_line( - "Topology", - &format!( - "{} cores across {} core types", - cpu_info.total_cores(), - cpu_info.cores.len() - ), + disp.display_topology_line( + cpu_info.total_cores(), + cpu_info.total_threads(), + true, + cpu_info.cores.len(), ); for (i, core) in cpu_info.cores.iter().enumerate() { @@ -60,24 +58,13 @@ impl CpuDisplay { disp.section_line("Count", &core.count.to_string()); - if let Some(speed) = &core.speed - && speed.base > 0 - { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - } else { - disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); let cc = |s| CpuDisplay::cache_count(s, core.count); disp.display_cache(core.cache, &cc, 0); @@ -87,36 +74,19 @@ impl CpuDisplay { } } } else if let Some(core) = cpu_info.cores.first() { - disp.simple_line("Topology", &format!("{} cores", core.count)); + disp.display_topology_line(core.count, core.threads, false, 1); let cc = |share_count: u32| -> String { CpuDisplay::cache_count(share_count, core.count) }; disp.display_cache(core.cache, &cc, 0); - if let Some(speed) = &core.speed - && speed.base > 0 - { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - disp.newline(); - } else { - disp.simple_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency(core.speed, flags); } // Display features - if !cpu_info.features.is_empty() { - let keys = [ + disp.display_features( + &cpu_info.features, + &[ "Mul", "Atomic", "Float", @@ -127,19 +97,7 @@ impl CpuDisplay { "Priv", "Cache", "Misc", - ]; - let mut first = true; - for key in keys { - if let Some(feat_str) = cpu_info.features.get(key) { - if first { - println!("{}{}", disp.inline_sublabel("Features", key), feat_str); - first = false; - } else { - println!("{}{}", disp.sublabel(key), feat_str); - } - } - } - println!(); - } + ], + ); } } diff --git a/src/riscv/micro_arch.rs b/src/riscv/micro_arch.rs index 913d80f4..0e676805 100644 --- a/src/riscv/micro_arch.rs +++ b/src/riscv/micro_arch.rs @@ -4,20 +4,11 @@ //! vendor/architecture IDs to known CPU cores. use crate::common::CoreType; +use crate::common::UNK; use crate::common::constants::*; -use crate::common::{Cache, Speed, UNK}; use crate::riscv::brand::*; -#[derive(Debug, Clone, PartialEq)] -pub struct CpuCore { - pub kind: CoreType, - pub micro_arch: MicroArch, - pub name: Option, - pub cache: Option, - pub speed: Option, - pub count: u32, - pub threads: u32, -} +pub type CpuCore = crate::common::CpuCore; /// RISC-V `misa` register layout. /// diff --git a/src/riscv/os/linux.rs b/src/riscv/os/linux.rs index f44d1007..d0af42b3 100644 --- a/src/riscv/os/linux.rs +++ b/src/riscv/os/linux.rs @@ -142,6 +142,7 @@ pub fn detect() -> OsCpuInfo { kind: core_type, micro_arch: cpu_arch.micro_arch, name: Some(String::from(cpu_arch.micro_arch)), + implementer: None, cache, speed, count: core_count, diff --git a/src/riscv/os/mod.rs b/src/riscv/os/mod.rs index 17c5dfad..96ee076e 100644 --- a/src/riscv/os/mod.rs +++ b/src/riscv/os/mod.rs @@ -1,3 +1,4 @@ +use super::micro_arch::CpuCore; use super::micro_arch::*; use crate::common::*; use std::collections::BTreeMap; diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index b2465293..8dcca551 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -6,7 +6,6 @@ use super::topology::Topology; use super::vendor::Cyrix; use super::*; use crate::common::{Cache, CoreType, DataSource, Speed, TDetect, UNK}; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; @@ -228,31 +227,9 @@ impl CpuSignature { } } -/// Information about a specific core type/cluster in the CPU. -#[derive(Debug, Default, Clone, PartialEq)] -pub struct CpuCore { - /// Classification of this core (Performance, Efficiency, Super) - pub kind: CoreType, - /// Microarchitecture variant of this core type - pub micro_arch: MicroArch, - /// Human-readable name for this microarchitecture (e.g., "Golden Cove") - pub name: Option<&'static str>, - /// Cache hierarchy specific to this core type - pub cache: Option, - /// Clock speed for this specific core type (base and boost frequencies) - pub speed: Option, - /// Number of physical cores of this type - pub count: u32, - /// Number of logical threads of this type - pub threads: u32, -} - -/// Represents a complete x86/x86_64 CPU with all detected information. +/// x86 architecture-specific data. #[derive(Debug, Default, PartialEq)] -pub struct Cpu { - /// The system name, if applicable - #[cfg(not(dos_os))] - pub system: Option, +pub struct X86Data { /// Does this cpu have cpuid instruction support pub has_cpuid: bool, /// CPU architecture and microarchitecture details @@ -265,14 +242,13 @@ pub struct Cpu { pub brand_id: u32, /// CPU signature (family, model, stepping) pub signature: CpuSignature, - /// Detected CPU features - pub features: BTreeMap<&'static str, String>, /// Speed, threads, cores, sockets pub topology: Topology, - /// Per-core-type breakdown of CPU cores - pub cores: Vec, } +pub type CpuCore = crate::common::CpuCore; +pub type Cpu = crate::common::Cpu; + impl Cpu { /// Gets the CPU model string. pub fn raw_model_string() -> String { @@ -507,7 +483,7 @@ impl Cpu { String::from(s) } - fn easter_egg() -> Option { + pub(crate) fn easter_egg() -> Option { let mut out: String = String::new(); let brand = CpuBrand::detect(); @@ -587,9 +563,7 @@ impl TDetect for Cpu { #[cfg(dos_os)] let cores = Self::fallback_homogeneous(&arch, &topology); - Self { - #[cfg(not(dos_os))] - system, + let extra = X86Data { has_cpuid: (is_cyrix() && Cyrix::can_enable_cpuid()) || has_cpuid(), arch, hyp_vendor_str: if is_hypervisor_guest() && max_hypervisor_leaf() > 0 { @@ -600,9 +574,19 @@ impl TDetect for Cpu { easter_egg: Self::easter_egg(), brand_id: get_brand_id(), signature: sig, - features: get_feature_list(), topology, + }; + + Self { + #[cfg(not(dos_os))] + system, + #[cfg(dos_os)] + system: None, + vendor: String::from(extra.arch.brand_name), + model: extra.arch.model.clone(), cores, + features: get_feature_list(), + extra, } } } @@ -617,7 +601,7 @@ impl Cpu { let cores_per_socket = (topology.cores.count / sockets).max(1); let threads_per_socket = (topology.threads.count / sockets).max(1); let name = if arch.code_name != UNK { - Some(arch.code_name) + Some(String::from(arch.code_name)) } else { None }; @@ -625,17 +609,13 @@ impl Cpu { kind: CoreType::Performance, micro_arch: arch.micro_arch, name, + implementer: None, cache, speed: speed_opt, count: cores_per_socket, threads: threads_per_socket, }] } - - /// Returns true if this CPU has multiple core types (hybrid architecture). - pub fn is_hybrid(&self) -> bool { - self.cores.len() > 1 - } } #[cfg(not(dos_os))] @@ -654,7 +634,7 @@ impl Cpu { fn find_or_push( cores: &mut Vec, core_type: CoreType, - name: Option<&'static str>, + name: Option, micro_arch: MicroArch, speed: Option, cache: Option, @@ -675,6 +655,7 @@ impl Cpu { kind: core_type, micro_arch, name, + implementer: None, cache, speed, count, @@ -705,7 +686,7 @@ impl Cpu { let name_str = micro_arch.as_str(); let name = if name_str != UNK { - Some(name_str) + Some(String::from(name_str)) } else { None }; @@ -743,7 +724,7 @@ impl Cpu { let name_str = micro_arch.as_str(); let name = if name_str != UNK { - Some(name_str) + Some(String::from(name_str)) } else { None }; @@ -790,7 +771,7 @@ impl Cpu { let name_str = micro_arch.as_str(); let name = if name_str != UNK { - Some(name_str) + Some(String::from(name_str)) } else { None }; @@ -839,24 +820,30 @@ mod tests { }; let cpu_am486_dx2 = Cpu { - arch: arch_am486.clone(), - brand_id: 0, - easter_egg: None, - signature: dummy_sig, + extra: X86Data { + arch: arch_am486.clone(), + brand_id: 0, + easter_egg: None, + signature: dummy_sig, + topology: Topology::default(), + ..Default::default() + }, features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!(cpu_am486_dx2.display_model_string(), "AMD 486 DX2"); arch_am486.code_name = "Am486X2WB"; let cpu_am486_x2wb = Cpu { - arch: arch_am486.clone(), - brand_id: 0, - easter_egg: None, - signature: dummy_sig, + extra: X86Data { + arch: arch_am486.clone(), + brand_id: 0, + easter_egg: None, + signature: dummy_sig, + topology: Topology::default(), + ..Default::default() + }, features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!( @@ -866,33 +853,39 @@ mod tests { // Test case for MicroArch::I486 let cpu_i486_dx = Cpu { - arch: CpuArch { - micro_arch: MicroArch::I486, - code_name: "i80486DX", - brand_name: "Intel", - vendor_string: String::from(VENDOR_INTEL), + extra: X86Data { + arch: CpuArch { + micro_arch: MicroArch::I486, + code_name: "i80486DX", + brand_name: "Intel", + vendor_string: String::from(VENDOR_INTEL), + ..Default::default() + }, + brand_id: 0, + easter_egg: None, + signature: dummy_sig, + topology: Topology::default(), ..Default::default() }, - brand_id: 0, - easter_egg: None, - signature: dummy_sig, features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!(cpu_i486_dx.display_model_string(), "Intel 486 DX"); // Test case for "No CPUID" let cpu_no_cpuid = Cpu { - arch: CpuArch { - vendor_string: String::from("UnknownVendor"), - ..CpuArch::default() + extra: X86Data { + arch: CpuArch { + vendor_string: String::from("UnknownVendor"), + ..CpuArch::default() + }, + brand_id: 0, + easter_egg: None, + signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), + topology: Topology::default(), + ..Default::default() }, - brand_id: 0, - easter_egg: None, - signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!(cpu_no_cpuid.display_model_string(), UNK); @@ -902,16 +895,19 @@ mod tests { fn test_display_model_string() { // Test case for "Unknown" let cpu_unknown = Cpu { - arch: CpuArch { - model: String::from("Unknown"), - vendor_string: String::from("UnknownVendor"), - ..CpuArch::default() + extra: X86Data { + arch: CpuArch { + model: String::from("Unknown"), + vendor_string: String::from("UnknownVendor"), + ..CpuArch::default() + }, + brand_id: 0, + easter_egg: None, + signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), + topology: Topology::default(), + ..Default::default() }, - brand_id: 0, - easter_egg: None, - signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!(cpu_unknown.display_model_string(), "Unknown"); diff --git a/src/x86/display.rs b/src/x86/display.rs index 2f398e49..96f23c47 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -121,24 +121,13 @@ impl Cpu { disp.section_line("Topology", &format!("{} cores", core.count)); } - if let Some(speed) = &core.speed - && speed.base > 0 - { - if speed.boost > speed.base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(speed.base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(speed.boost) - ); - } else { - disp.section_line("Frequency", &CpuDisplay::format_frequency(speed.base)); - } - } + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); let smt = cpuid_threads_per_core() .max(core.threads / core.count.max(1)) @@ -193,34 +182,14 @@ impl Cpu { } } - fn print_speed(&self, disp: &CpuDisplay) { + fn print_speed(&self, flags: CliFlags, disp: &CpuDisplay) { let speed = self .cores .first() - .and_then(|c| c.speed.as_ref()) - .unwrap_or(&self.topology.speed); - - if speed.base > 0 { - let base = speed.base; - let boost = speed.boost; - - if boost > base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(boost) - ); - } else { - disp.section_line("Frequency", &CpuDisplay::format_frequency(base)); - } + .and_then(|c| c.speed) + .unwrap_or(self.topology.speed); - disp.newline(); - } + disp.display_frequency(Some(speed), flags); } fn print_signature(&self, flags: CliFlags, disp: &CpuDisplay) { @@ -549,7 +518,7 @@ impl TCpuDisplay for Cpu { // Clock Speed (Base/Boost) if !self.is_hybrid() { - self.print_speed(&disp); + self.print_speed(flags, &disp); } // CPU Signature diff --git a/src/x86/dump.rs b/src/x86/dump.rs index 633f5808..5071954b 100644 --- a/src/x86/dump.rs +++ b/src/x86/dump.rs @@ -1,6 +1,5 @@ use super::*; use super::{CENTAUR_LEAF_0, EXT_LEAF_0, TRANSMETA_LEAF_0, VENDOR_AMD}; -use crate::common::TDetect; use crate::x86; use core::fmt::Write; @@ -109,8 +108,7 @@ pub fn dump_cpu(f: &mut impl Write, cpu_idx: usize) { let vendor = vendor_str(); - let easter_egg = Cpu::detect().easter_egg; - if easter_egg.is_some() { + if Cpu::easter_egg().is_some() { match &*vendor { VENDOR_AMD => dump_leaf(f, AMD_EASTER_EGG_ADDR, 0, 4), VENDOR_RISE | VENDOR_SIS | VENDOR_DMP | VENDOR_RDC => { diff --git a/tests/cpuid_dump_test.rs b/tests/cpuid_dump_test.rs index 07d69263..85d08cab 100644 --- a/tests/cpuid_dump_test.rs +++ b/tests/cpuid_dump_test.rs @@ -2,6 +2,7 @@ use rustid::common::TDetect; use rustid::common::*; +use rustid::x86::Cpu; use rustid::x86::provider::*; use rustid::x86::*; use std::path::PathBuf; @@ -1007,10 +1008,10 @@ cpuid_testsuite!( assert_eq!(cpu.cores.len(), 2); assert_eq!(cpu.cores[0].kind, CoreType::Performance); assert_eq!(cpu.cores[0].micro_arch, MicroArch::GoldenCove); - assert_eq!(cpu.cores[0].name, Some("Golden Cove")); + assert_eq!(cpu.cores[0].name.as_deref(), Some("Golden Cove")); assert_eq!(cpu.cores[1].kind, CoreType::Efficiency); assert_eq!(cpu.cores[1].micro_arch, MicroArch::Gracemont); - assert_eq!(cpu.cores[1].name, Some("Gracemont")); + assert_eq!(cpu.cores[1].name.as_deref(), Some("Gracemont")); } } ); From e9daae3893ac36095c702c519fe5812320547c34 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 18:24:07 -0400 Subject: [PATCH 06/30] Silence clippy warning --- src/arm/display.rs | 14 +++++---- src/common/display.rs | 37 +++++++++++----------- src/x86/cpu.rs | 73 +++++++++++++++++++++++++------------------ src/x86/dump.rs | 2 +- 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/src/arm/display.rs b/src/arm/display.rs index 373d8564..133062a7 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -192,9 +192,10 @@ impl CpuDisplay { println!("{}", disp.label(&core_num)); if let Some(ref vendor_str) = core.implementer - && vendor_str != UNK { - disp.section_line("Implementer", vendor_str); - } + && vendor_str != UNK + { + disp.section_line("Implementer", vendor_str); + } let name = Into::<&str>::into(core.kind); disp.section_line("Type", name); @@ -231,9 +232,10 @@ impl CpuDisplay { println!("{}", disp.label("Cores")); if let Some(ref vendor_str) = core.implementer - && vendor_str != UNK { - disp.section_line("Implementer", vendor_str); - } + && vendor_str != UNK + { + disp.section_line("Implementer", vendor_str); + } let ma_str: String = core.micro_arch.into(); if Self::should_show_core_micro_arch(core.micro_arch, flags.verbose) { diff --git a/src/common/display.rs b/src/common/display.rs index 2812a536..c0985370 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -102,25 +102,26 @@ impl CpuDisplay { /// Displays frequency lines (Base/Boost inline sublabels or single section line). pub fn display_frequency(&self, speed: Option, flags: CliFlags) { if let Some(speed) = speed - && speed.base > 0 { - if speed.boost > speed.base { - println!( - "{}{}", - self.inline_sublabel("Frequency", "Base"), - Self::format_frequency(speed.base) - ); - println!( - "{}{}", - self.sublabel("Boost"), - Self::format_frequency(speed.boost) - ); - } else { - self.section_line("Frequency", &Self::format_frequency(speed.base)); - } - if !flags.compact { - self.newline(); - } + && speed.base > 0 + { + if speed.boost > speed.base { + println!( + "{}{}", + self.inline_sublabel("Frequency", "Base"), + Self::format_frequency(speed.base) + ); + println!( + "{}{}", + self.sublabel("Boost"), + Self::format_frequency(speed.boost) + ); + } else { + self.section_line("Frequency", &Self::format_frequency(speed.base)); + } + if !flags.compact { + self.newline(); } + } } /// Displays the Topology line for homogeneous or hybrid configurations. diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index 8dcca551..0134fe4d 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -631,36 +631,17 @@ impl Cpu { let mut cores: Vec = Vec::new(); - fn find_or_push( - cores: &mut Vec, - core_type: CoreType, - name: Option, - micro_arch: MicroArch, - speed: Option, - cache: Option, - count: u32, - threads: u32, - ) { - if let Some(c) = cores - .iter_mut() - .find(|c| c.kind == core_type && c.micro_arch == micro_arch && c.name == name) - { - c.count += count; - c.threads += threads; - if c.speed.is_none() && speed.is_some() { - c.speed = speed; + fn find_or_push(cores: &mut Vec, core: CpuCore) { + if let Some(c) = cores.iter_mut().find(|c| { + c.kind == core.kind && c.micro_arch == core.micro_arch && c.name == core.name + }) { + c.count += core.count; + c.threads += core.threads; + if c.speed.is_none() && core.speed.is_some() { + c.speed = core.speed; } } else { - cores.push(CpuCore { - kind: core_type, - micro_arch, - name, - implementer: None, - cache, - speed, - count, - threads, - }); + cores.push(core); } } @@ -696,7 +677,17 @@ impl Cpu { let speed_opt = if speed.base > 0 { Some(speed) } else { None }; find_or_push( - &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + &mut cores, + CpuCore { + kind: core_type, + micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: 1, + threads: 1, + }, ); } } @@ -734,7 +725,17 @@ impl Cpu { let speed_opt = if speed.base > 0 { Some(speed) } else { None }; find_or_push( - &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + &mut cores, + CpuCore { + kind: core_type, + micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: 1, + threads: 1, + }, ); } } @@ -780,7 +781,17 @@ impl Cpu { let speed_opt = if speed.base > 0 { Some(speed) } else { None }; find_or_push( - &mut cores, core_type, name, micro_arch, speed_opt, cache, 1, 1, + &mut cores, + CpuCore { + kind: core_type, + micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: 1, + threads: 1, + }, ); } } diff --git a/src/x86/dump.rs b/src/x86/dump.rs index 5071954b..fe9e88b8 100644 --- a/src/x86/dump.rs +++ b/src/x86/dump.rs @@ -109,7 +109,7 @@ pub fn dump_cpu(f: &mut impl Write, cpu_idx: usize) { let vendor = vendor_str(); if Cpu::easter_egg().is_some() { - match &*vendor { + match vendor.as_str() { VENDOR_AMD => dump_leaf(f, AMD_EASTER_EGG_ADDR, 0, 4), VENDOR_RISE | VENDOR_SIS | VENDOR_DMP | VENDOR_RDC => { dump_leaf(f, RISE_EASTER_EGG_ADDR, 0, 4); From c623fc25e484295d56bd688b20ff86a0abe9ed76 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 18:47:00 -0400 Subject: [PATCH 07/30] Bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4e326c2..b68de18a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,7 +55,7 @@ dependencies = [ [[package]] name = "rustid" -version = "2.0.0" +version = "2.1.0" dependencies = [ "core_affinity", "windows", diff --git a/Cargo.toml b/Cargo.toml index 575da216..83c7a3ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustid" -version = "2.0.0" +version = "2.1.0" edition = "2024" authors = ["Timothy J. Warren "] description = "A utility to identify the name and properties of the current cpu" From 57ff439c095819be4d64ff1e27a94e1a11131c12 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 22:15:34 -0400 Subject: [PATCH 08/30] Fix dos socket detection: treat MP Table entries as logical processors, and use other cpuid data to calculate sockets --- src/x86/count.rs | 8 +++ src/x86/dos/mp.rs | 129 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/x86/count.rs b/src/x86/count.rs index c6403218..fa617adf 100644 --- a/src/x86/count.rs +++ b/src/x86/count.rs @@ -127,6 +127,14 @@ pub fn get_thread_count() -> TopologyTier { } fn get_platform_thread_count() -> TopologyTier { + #[cfg(any(dos, dos32a))] + { + let count = crate::x86::dos::mp::MpTable::detect().processor_count(); + if count > 0 { + return TopologyTier::new(count, DataSource::MpTable); + } + } + #[cfg(target_os = "uefi")] if let Some(mp) = crate::x86::efi::mp::EfiMpServices::detect() { let count = mp.processor_count() as u32; diff --git a/src/x86/dos/mp.rs b/src/x86/dos/mp.rs index b6c15249..814101c5 100644 --- a/src/x86/dos/mp.rs +++ b/src/x86/dos/mp.rs @@ -7,21 +7,28 @@ /// MultiProcessor (MP) table information for multi-socket systems. #[derive(Debug)] pub struct MpTable { - /// Number of processor sockets - pub sockets: u32, + /// Number of enabled processors (logical cores/threads) + pub processors: u32, } impl Default for MpTable { fn default() -> MpTable { - MpTable { sockets: 1 } + MpTable { processors: 1 } } } impl MpTable { + /// Returns the number of enabled processors. + #[must_use] + pub fn processor_count(&self) -> u32 { + self.processors + } + /// Returns the number of processor sockets. #[must_use] pub fn socket_count(&self) -> u32 { - self.sockets + let threads_per_pkg = crate::x86::cpuid_threads_per_package().max(1); + (self.processors / threads_per_pkg).max(1) } } @@ -83,7 +90,7 @@ fn peek_u16_so(seg: u16, off: u16) -> u16 { impl MpTable { /// Detects the number of sockets using the Intel MP Specification. pub fn detect() -> MpTable { - let mut table = MpTable { sockets: 1 }; + let mut table = MpTable { processors: 1 }; // MP Table lookup is only applicable to certain CPUs if !(crate::x86::is_intel() || crate::x86::is_vortex() || crate::x86::is_centaur()) { @@ -95,10 +102,10 @@ impl MpTable { if mpfp.config_table_ptr != 0 && let Some(count) = Self::parse_config_table(mpfp.config_table_ptr) { - table.sockets = count; + table.processors = count; } else if mpfp.mp_feature1 != 0 { // Default configurations (1-7) all have 2 CPUs - table.sockets = 2; + table.processors = 2; } } @@ -126,19 +133,39 @@ impl MpTable { return None; } - let entry_count = peek_u16_so(seg, off + 34); - let mut sockets = 0; - let mut current_off = off + 44; + let mut buf = [0u8; 512]; + for (i, b) in buf.iter_mut().enumerate() { + if (off as usize + i) > 0xFFFF { + break; + } + *b = peek_u8_so(seg, off + i as u16); + } + + Self::parse_pcmp_slice(&buf) + } + + /// Parses a PCMP configuration table buffer and returns the number of enabled processors. + pub fn parse_pcmp_slice(bytes: &[u8]) -> Option { + if bytes.len() < 44 || &bytes[0..4] != b"PCMP" { + return None; + } + + let entry_count = u16::from_le_bytes([bytes[34], bytes[35]]); + let mut processors = 0; + let mut current_off = 44; for _ in 0..entry_count { - if current_off > 0xFFF0 { + if current_off >= bytes.len() { break; } - let entry_type = peek_u8_so(seg, current_off); + let entry_type = bytes[current_off]; if entry_type == 0 { - let flags = peek_u8_so(seg, current_off + 3); + if current_off + 3 >= bytes.len() { + break; + } + let flags = bytes[current_off + 3]; if (flags & 0x01) != 0 { - sockets += 1; + processors += 1; } current_off += 20; } else { @@ -146,7 +173,7 @@ impl MpTable { } } - if sockets > 0 { Some(sockets) } else { None } + if processors > 0 { Some(processors) } else { None } } #[inline(never)] @@ -238,3 +265,75 @@ impl MpTable { if seg != 0 { Some(seg) } else { None } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mp_table_default() { + let mp = MpTable::default(); + assert_eq!(mp.processor_count(), 1); + } + + #[test] + fn test_parse_pcmp_slice_quad_core() { + let mut data = [0u8; 124]; + // Signature "PCMP" + data[0..4].copy_from_slice(b"PCMP"); + // Entry count: 4 + data[34..36].copy_from_slice(&4u16.to_le_bytes()); + + // 4 processor entries starting at offset 44 (20 bytes each) + for i in 0..4 { + let off = 44 + i * 20; + data[off] = 0; // Entry type 0: Processor + data[off + 1] = i as u8; // APIC ID + data[off + 3] = 0x01; // Flags: Enabled (bit 0 = 1) + } + + assert_eq!(MpTable::parse_pcmp_slice(&data), Some(4)); + } + + #[test] + fn test_parse_pcmp_slice_with_disabled_core() { + let mut data = [0u8; 124]; + data[0..4].copy_from_slice(b"PCMP"); + data[34..36].copy_from_slice(&4u16.to_le_bytes()); + + for i in 0..4 { + let off = 44 + i * 20; + data[off] = 0; // Processor entry + data[off + 3] = if i == 3 { 0x00 } else { 0x01 }; // 4th processor is disabled + } + + assert_eq!(MpTable::parse_pcmp_slice(&data), Some(3)); + } + + #[test] + fn test_parse_pcmp_slice_mixed_entries() { + let mut data = [0u8; 92]; + data[0..4].copy_from_slice(b"PCMP"); + data[34..36].copy_from_slice(&3u16.to_le_bytes()); + + // Entry 0: Processor (20 bytes) + data[44] = 0; + data[47] = 0x01; + + // Entry 1: Processor (20 bytes) + data[64] = 0; + data[67] = 0x01; + + // Entry 2: Bus entry (Type 1, 8 bytes) + data[84] = 1; + + assert_eq!(MpTable::parse_pcmp_slice(&data), Some(2)); + } + + #[test] + fn test_parse_pcmp_slice_invalid_signature() { + let mut data = [0u8; 64]; + data[0..4].copy_from_slice(b"INVALID"); + assert_eq!(MpTable::parse_pcmp_slice(&data), None); + } +} From 444745a4371fd918f31e63e2f36ed3c5f1ee1ac6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 22:16:40 -0400 Subject: [PATCH 09/30] Fix some tests broken by the refactoring effort --- src/ppc/cpu.rs | 6 ++++++ src/riscv/os/mod.rs | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index 1e45fff9..31f8d1a6 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -120,6 +120,7 @@ impl Cpu { None } + #[cfg(target_os = "linux")] fn detect_clock_speed_from_cpuinfo() -> Option { let cpuinfo = get_proc_cpuinfo_data(); for map in &cpuinfo { @@ -133,6 +134,11 @@ impl Cpu { None } + #[cfg(not(target_os = "linux"))] + fn detect_clock_speed_from_cpuinfo() -> Option { + None + } + fn parse_mhz_value(value: &str) -> Option { let value = value.trim(); let value = value.trim_end_matches("MHz").trim().trim_end_matches("MHz"); diff --git a/src/riscv/os/mod.rs b/src/riscv/os/mod.rs index 96ee076e..6ebb4b35 100644 --- a/src/riscv/os/mod.rs +++ b/src/riscv/os/mod.rs @@ -23,3 +23,25 @@ pub struct OsCpuInfo { pub mod linux; #[cfg(any(target_os = "android", target_os = "linux"))] pub use linux::*; + +#[cfg(not(any(target_os = "android", target_os = "linux")))] +pub mod fallback { + use super::*; + pub fn detect() -> OsCpuInfo { + OsCpuInfo { + vendor: String::new(), + cpu_arch: CpuArch::default(), + model: String::new(), + isa_string: String::new(), + cores: Vec::new(), + raw: BTreeMap::new(), + midr_source: DataSource::default(), + features_source: DataSource::default(), + } + } + pub fn get_all_features(_isa: &str) -> BTreeMap<&'static str, String> { + BTreeMap::new() + } +} +#[cfg(not(any(target_os = "android", target_os = "linux")))] +pub use fallback::*; From 8819eca17ae4a87ae735f0c8c3a61f079a8863e8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Thu, 27 Aug 2026 22:27:39 -0400 Subject: [PATCH 10/30] Add changelog entry for dos topology fix --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e99673b1..88b6a0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [2.1.0] + +### Fixed +- DOS Topology detection using Intel MPTables was assuming one APIC id = 1 socket, rather than one APIC id = 1 logical cpu. (This caused a Core 2 Quad to show 4 sockets, 16 cores, 16 threads) + ## [2.0.0] — Add missing Intel and AMD cpu mappings, fix edge cases, and more ### Added From 6d55948979e7edb737cf1d58e29b6bbddf5d9323 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 08:57:20 -0400 Subject: [PATCH 11/30] Simplify compile guard --- src/x86/cpu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index 0134fe4d..fe40ceab 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -12,7 +12,7 @@ use alloc::vec::Vec; #[cfg(not(nostd_os))] use super::provider; -#[cfg(any(not(nostd_os), target_os = "uefi"))] +#[cfg(not(dos_os))] use crate::common::TOSData; /// CPU feature class/level enumeration. From faf046ead77f1d43e416f30d67da9ff3d7abed84 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 10:55:00 -0400 Subject: [PATCH 12/30] Separate cpuid detection from OS detection, simplify compile guards --- build.rs | 21 ++- src/common/cache.rs | 11 +- src/common/os/android.rs | 1 + src/common/os/efi.rs | 2 +- src/common/os/linux.rs | 3 + src/common/os/mod.rs | 2 +- src/dos_rustid.rs | 14 +- src/efi_rustid.rs | 10 +- src/lib.rs | 32 ++-- src/rust86.rs | 12 +- src/rustid.rs | 2 +- src/x86/cache.rs | 2 +- src/x86/count.rs | 98 ++--------- src/x86/cpu.rs | 339 +++++++++++++++----------------------- src/x86/display.rs | 24 +-- src/x86/dos/allocator.rs | 4 +- src/x86/dos/args.rs | 10 +- src/x86/dos/cache.rs | 2 +- src/x86/dos/mod.rs | 76 +++++++-- src/x86/dos/mp.rs | 18 +- src/x86/dos/speed.rs | 4 +- src/x86/dump.rs | 2 +- src/x86/efi/display.rs | 2 +- src/x86/efi/mod.rs | 158 +++++++++++++++++- src/x86/efi/mp.rs | 2 +- src/x86/efi/os.rs | 4 +- src/x86/efi/smbios.rs | 8 +- src/x86/features.rs | 12 +- src/x86/fns.rs | 12 +- src/x86/mod.rs | 11 +- src/x86/os/mod.rs | 233 ++++++++++++++++++++++++++ src/x86/provider.rs | 4 + src/x86/quirks.rs | 8 +- src/x86/topology.rs | 246 ++++++++++++++++++--------- src/x86/vendor/amd.rs | 20 +-- src/x86/vendor/centaur.rs | 8 +- src/x86/vendor/cyrix.rs | 8 +- src/x86/vendor/intel.rs | 26 +-- tests/cpuid_dump_test.rs | 68 ++++++++ 39 files changed, 994 insertions(+), 525 deletions(-) create mode 100644 src/x86/os/mod.rs diff --git a/build.rs b/build.rs index 99fbc379..ba596957 100644 --- a/build.rs +++ b/build.rs @@ -210,13 +210,24 @@ macro_rules! cfg_aliases { fn main() { // Setup cfg aliases cfg_aliases! { - dos: { all(target_os = "none", target_arch= "x86", not(feature = "dos32a-build")) }, - dos32a: { all(target_os = "none", target_arch= "x86", feature = "dos32a-build") }, - dos_os: { all(target_os = "none", target_arch="x86") }, + // Runtime / Environment Model + uefi: { target_os = "uefi" }, + dos_real: { all(target_os = "none", target_arch = "x86", not(feature = "dos32a-build")) }, + dos_ext: { all(target_os = "none", target_arch = "x86", feature = "dos32a-build") }, + dos_os: { all(target_os = "none", target_arch = "x86") }, nostd_os: { any(target_os = "none", target_os = "uefi") }, + std_os: { not(any(target_os = "none", target_os = "uefi")) }, + + // Operating System Families bsd: { any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd") }, - arm_cpu: { any(target_arch = "arm", target_arch="aarch64", target_arch="arm64ec") }, + linux_os: { any(target_os = "android", target_os = "linux") }, + unix_os: { any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd") }, + windows_os: { target_os = "windows" }, + + // CPU Architectures + x86_cpu: { any(target_arch = "x86", target_arch = "x86_64") }, + arm_cpu: { any(target_arch = "arm", target_arch = "aarch64", target_arch = "arm64ec") }, ppc_cpu: { any(target_arch = "powerpc", target_arch = "powerpc64") }, - x86_cpu: { any(target_arch = "x86", target_arch = "x86_64") } + riscv_cpu: { any(target_arch = "riscv32", target_arch = "riscv64") } } } diff --git a/src/common/cache.rs b/src/common/cache.rs index 7eea290e..57a48d73 100644 --- a/src/common/cache.rs +++ b/src/common/cache.rs @@ -174,18 +174,11 @@ impl Cache { /// Detects cache using platform/OS specific information sources. #[must_use] pub fn detect_os() -> Option { - #[cfg(any(target_os = "android", target_os = "linux"))] + #[cfg(all(linux_os, not(x86_cpu)))] { - #[cfg(x86_cpu)] - if crate::x86::provider::info_source() - == crate::x86::provider::CpuidInfoSource::DumpFile - { - return None; - } - Cache::from_sys_fs() } - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(all(linux_os, not(x86_cpu))))] { None } diff --git a/src/common/os/android.rs b/src/common/os/android.rs index 0f8d6e28..d25ec4be 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -433,6 +433,7 @@ impl TDetect for TopologyCount { // Cache Detection // ---------------------------------------------------------------------------- +#[cfg(any(not(x86_cpu), test))] impl Cache { #[cfg(not(x86_cpu))] pub fn detect() -> Option { diff --git a/src/common/os/efi.rs b/src/common/os/efi.rs index 7cec92d3..2815bc76 100644 --- a/src/common/os/efi.rs +++ b/src/common/os/efi.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] use crate::common::{OS, TOSData, TopologyTier}; use alloc::string::String; diff --git a/src/common/os/linux.rs b/src/common/os/linux.rs index 0c5305d4..2bfea87c 100644 --- a/src/common/os/linux.rs +++ b/src/common/os/linux.rs @@ -7,6 +7,7 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::Path; +#[cfg(any(not(x86_cpu), test))] use crate::common::{Cache, CacheLevel, CacheType, Level1Cache}; #[cfg(any(arm_cpu, test))] @@ -342,6 +343,7 @@ impl TDetect for TopologyCount { } } +#[cfg(any(not(x86_cpu), test))] impl Cache { #[cfg(not(x86_cpu))] pub fn detect() -> Option { @@ -356,6 +358,7 @@ impl Cache { None } + #[cfg(not(x86_cpu))] pub(crate) fn from_sys_fs() -> Option { Self::read_cpu_cache(0) } diff --git a/src/common/os/mod.rs b/src/common/os/mod.rs index 947490d4..46db2bfa 100644 --- a/src/common/os/mod.rs +++ b/src/common/os/mod.rs @@ -5,7 +5,7 @@ use alloc::string::String; #[cfg(bsd)] pub mod bsd; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] pub mod efi; pub mod common; diff --git a/src/dos_rustid.rs b/src/dos_rustid.rs index 3930df98..4dc6c302 100644 --- a/src/dos_rustid.rs +++ b/src/dos_rustid.rs @@ -1,10 +1,10 @@ -#![cfg_attr(all(not(test), dos32a), no_std)] -#![cfg_attr(all(not(test), dos32a), no_main)] +#![cfg_attr(all(not(test), dos_ext), no_std)] +#![cfg_attr(all(not(test), dos_ext), no_main)] -#[cfg(dos32a)] +#[cfg(dos_ext)] extern crate alloc; -#[cfg(dos32a)] +#[cfg(dos_ext)] #[unsafe(no_mangle)] #[unsafe(link_section = ".startup")] #[unsafe(naked)] @@ -22,7 +22,7 @@ pub unsafe extern "C" fn _start() -> ! { ); } -#[cfg(dos32a)] +#[cfg(dos_ext)] fn help() { use rustid::println; println!("Usage: RUSTID [/FLAGS] [COMMAND]"); @@ -41,7 +41,7 @@ fn help() { println!("Examples: RUSTID /E RUSTID /VERBOSE"); } -#[cfg(dos32a)] +#[cfg(dos_ext)] #[unsafe(no_mangle)] pub extern "C" fn rust_main() -> ! { use rustid::common::{CliFlags, TCpuDisplay, TDetect}; @@ -209,5 +209,5 @@ pub extern "C" fn rust_main() -> ! { exit(0); } -#[cfg(not(dos32a))] +#[cfg(not(dos_ext))] pub fn main() {} diff --git a/src/efi_rustid.rs b/src/efi_rustid.rs index 16faf27a..a9471b37 100644 --- a/src/efi_rustid.rs +++ b/src/efi_rustid.rs @@ -1,10 +1,10 @@ -#![cfg_attr(all(not(test), target_os = "uefi"), no_std)] -#![cfg_attr(all(not(test), target_os = "uefi"), no_main)] +#![cfg_attr(all(not(test), uefi), no_std)] +#![cfg_attr(all(not(test), uefi), no_main)] -#[cfg(target_os = "uefi")] +#[cfg(uefi)] extern crate alloc; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] #[unsafe(no_mangle)] pub unsafe extern "efiapi" fn efi_main( image_handle: *mut core::ffi::c_void, @@ -33,5 +33,5 @@ pub unsafe extern "efiapi" fn efi_main( 0 } -#[cfg(not(target_os = "uefi"))] +#[cfg(not(uefi))] pub fn main() {} diff --git a/src/lib.rs b/src/lib.rs index 72027498..beebb338 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,33 +25,33 @@ extern crate alloc; -#[cfg(not(dos))] +#[cfg(not(dos_real))] const APP: &str = "Rustid"; -#[cfg(dos)] +#[cfg(dos_real)] const APP: &str = "Rust86"; const VERSION: &str = env!("CARGO_PKG_VERSION"); -#[cfg(not(nostd_os))] +#[cfg(std_os)] const ARCH: &str = std::env::consts::ARCH; -#[cfg(any(dos, dos32a))] +#[cfg(dos_os)] const ARCH: &str = "x86"; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] const ARCH: &str = if cfg!(target_arch = "x86_64") { "x86_64" } else { "x86" }; -#[cfg(not(nostd_os))] +#[cfg(std_os)] const OS: &str = std::env::consts::OS; -#[cfg(any(dos, dos32a))] +#[cfg(dos_os)] const OS: &str = "DOS"; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] const OS: &str = "UEFI"; -#[cfg(not(nostd_os))] +#[cfg(std_os)] extern crate std; pub mod common; @@ -71,18 +71,18 @@ pub mod arm; #[cfg(arm_cpu)] pub use arm::Cpu; -#[cfg(any(target_arch = "riscv64", test))] +#[cfg(any(riscv_cpu, test))] pub mod riscv; -#[cfg(target_arch = "riscv64")] +#[cfg(riscv_cpu)] pub use riscv::Cpu; -#[cfg(any(dos, dos32a))] +#[cfg(dos_os)] pub use x86::dos::*; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] pub use x86::efi::*; -#[cfg(not(nostd_os))] +#[cfg(std_os)] pub use std::{print, println}; pub fn version() { @@ -92,13 +92,13 @@ pub fn version() { ); } -#[cfg(not(nostd_os))] +#[cfg(std_os)] #[cfg(x86_cpu)] pub fn file_version() { println!("--------------- Rustid {VERSION} ({ARCH}-{OS}:from-cpuid-dump) ---------------"); } -#[cfg(any(target_arch = "x86", dos, dos32a))] +#[cfg(any(x86_cpu, dos_os))] pub fn cyrix_cpuid_check() { use crate::println; diff --git a/src/rust86.rs b/src/rust86.rs index 048eec44..2a660214 100644 --- a/src/rust86.rs +++ b/src/rust86.rs @@ -1,7 +1,7 @@ -#![cfg_attr(all(not(test), dos), no_std)] -#![cfg_attr(all(not(test), dos), no_main)] +#![cfg_attr(all(not(test), dos_real), no_std)] +#![cfg_attr(all(not(test), dos_real), no_main)] -#[cfg(dos)] +#[cfg(dos_real)] #[unsafe(no_mangle)] #[unsafe(link_section = ".startup")] #[unsafe(naked)] @@ -24,7 +24,7 @@ pub unsafe extern "C" fn _start() -> ! { ); } -#[cfg(dos)] +#[cfg(dos_real)] fn help() { use rustid::println; println!("Usage: RUST86 [/FLAGS]"); @@ -35,7 +35,7 @@ fn help() { println!(" /?, /H, /HELP Show this help message"); } -#[cfg(dos)] +#[cfg(dos_real)] #[unsafe(no_mangle)] pub extern "C" fn rust_main() -> ! { use rustid::common::{CliFlags, TCpuDisplay, TDetect}; @@ -98,5 +98,5 @@ pub extern "C" fn rust_main() -> ! { exit(0); } -#[cfg(not(dos))] +#[cfg(not(dos_real))] pub fn main() {} diff --git a/src/rustid.rs b/src/rustid.rs index 3da2b45d..75ff4ea0 100644 --- a/src/rustid.rs +++ b/src/rustid.rs @@ -1,4 +1,4 @@ -#![cfg(not(any(dos, dos32a)))] +#![cfg(not(dos_os))] use rustid::common::{TCpuDisplay, TDetect}; use rustid::{Cpu, version}; diff --git a/src/x86/cache.rs b/src/x86/cache.rs index 4b9f4510..5a8cbb45 100644 --- a/src/x86/cache.rs +++ b/src/x86/cache.rs @@ -348,7 +348,7 @@ impl Cache { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn apply_descriptor(desc: u32, c: &mut Cache) { match desc { 0x49 => { diff --git a/src/x86/count.rs b/src/x86/count.rs index fa617adf..a8a9999d 100644 --- a/src/x86/count.rs +++ b/src/x86/count.rs @@ -1,24 +1,21 @@ -#[cfg(any(dos, dos32a, target_os = "uefi"))] +#[cfg(nostd_os)] use crate::common::DataSource; use crate::common::TopologyTier; use crate::x86::{ cpuid_cores_per_package, cpuid_data_source, cpuid_threads_per_core, cpuid_threads_per_package, }; -#[cfg(not(nostd_os))] -use crate::common::{OS, TOSData}; - -#[cfg(not(nostd_os))] +#[cfg(std_os)] use super::{info_source, provider::CpuidInfoSource}; pub fn get_platform_socket_count() -> TopologyTier { - #[cfg(any(dos, dos32a))] + #[cfg(dos_os)] let sockets_detected = TopologyTier::new( crate::x86::dos::mp::MpTable::detect().socket_count(), DataSource::MpTable, ); - #[cfg(target_os = "uefi")] + #[cfg(uefi)] let mut sockets_detected = { let threads_per_pkg = cpuid_threads_per_package().max(1); let cores_per_pkg = cpuid_cores_per_package().max(1); @@ -54,7 +51,6 @@ pub fn get_platform_socket_count() -> TopologyTier { DataSource::Calculated("SMBIOS"), ) } else { - // Legacy SMBIOS (e.g. SMBIOS 2.4 where each core/thread is a separate Type 4 record) let mut unique_sockets = alloc::vec::Vec::new(); for p in &populated { if let Some(desig) = &p.socket_designation { @@ -91,14 +87,14 @@ pub fn get_platform_socket_count() -> TopologyTier { } }; - #[cfg(not(nostd_os))] + #[cfg(std_os)] let sockets_detected = if info_source() == CpuidInfoSource::Cpu { - OS::get_socket_count() + super::os::get_socket_count() } else { - TopologyTier::default() + TopologyTier::new(1, cpuid_data_source()) }; - #[cfg(target_os = "uefi")] + #[cfg(uefi)] { if let Some(smbios) = crate::x86::efi::smbios::detect_smbios() { if smbios.is_laptop() { @@ -111,88 +107,18 @@ pub fn get_platform_socket_count() -> TopologyTier { } pub fn get_thread_count() -> TopologyTier { - let platform_threads = get_platform_thread_count(); let pkg_threads = cpuid_threads_per_package(); - - if platform_threads.count > 0 { - TopologyTier::new( - platform_threads.count.max(pkg_threads), - platform_threads.source, - ) - } else if pkg_threads > 0 { - TopologyTier::new(pkg_threads, cpuid_data_source()) - } else { - TopologyTier::default() - } -} - -fn get_platform_thread_count() -> TopologyTier { - #[cfg(any(dos, dos32a))] - { - let count = crate::x86::dos::mp::MpTable::detect().processor_count(); - if count > 0 { - return TopologyTier::new(count, DataSource::MpTable); - } - } - - #[cfg(target_os = "uefi")] - if let Some(mp) = crate::x86::efi::mp::EfiMpServices::detect() { - let count = mp.processor_count() as u32; - if count > 0 { - return TopologyTier::new(count, DataSource::Calculated("EFI MP Services")); - } - } - - #[cfg(target_os = "uefi")] - if let Some(smbios) = crate::x86::efi::smbios::detect_smbios() { - let threads_per_pkg = cpuid_threads_per_package().max(1); - let populated = smbios - .processors - .iter() - .filter(|p| p.is_populated && p.is_enabled) - .collect::>(); - - let has_multi_core_field = populated - .iter() - .any(|p| p.core_count > 1 || p.thread_count > 1); - - let total_threads: u32 = if has_multi_core_field { - populated - .iter() - .map(|p| { - if p.thread_count > 0 { - p.thread_count - } else { - threads_per_pkg - } - }) - .sum() - } else { - // If legacy SMBIOS has multiple Type 4 entries (one per core), populated.len() is already the core/thread count - populated.len() as u32 - }; - - if total_threads > 0 { - return TopologyTier::new(total_threads, DataSource::Calculated("SMBIOS")); - } - } - - TopologyTier::default() + TopologyTier::new(pkg_threads.max(1), cpuid_data_source()) } pub fn get_core_count() -> TopologyTier { - let threads_tier = get_thread_count(); - let t_count = threads_tier.count; + let t_count = cpuid_threads_per_package(); let t_per_core = cpuid_threads_per_core(); if t_per_core > 1 && t_count > 1 { - TopologyTier::new(t_count / t_per_core, threads_tier.source) + TopologyTier::new(t_count / t_per_core, cpuid_data_source()) } else { let pkg_cores = cpuid_cores_per_package(); - if t_count < pkg_cores && pkg_cores > 0 { - TopologyTier::new(pkg_cores, cpuid_data_source()) - } else { - threads_tier - } + TopologyTier::new(pkg_cores.max(1), cpuid_data_source()) } } diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index fe40ceab..2fcb5c4d 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -5,16 +5,15 @@ use super::micro_arch::{CpuArch, MicroArch}; use super::topology::Topology; use super::vendor::Cyrix; use super::*; -use crate::common::{Cache, CoreType, DataSource, Speed, TDetect, UNK}; +#[cfg(std_os)] +use crate::common::{Cache, Speed}; +use crate::common::{CoreType, DataSource, TDetect, UNK}; use alloc::string::String; use alloc::vec::Vec; -#[cfg(not(nostd_os))] +#[cfg(std_os)] use super::provider; -#[cfg(not(dos_os))] -use crate::common::TOSData; - /// CPU feature class/level enumeration. /// /// Represents the instruction set and feature level of an x86 processor, @@ -170,9 +169,9 @@ impl CpuSignature { let is_overdrive = super::is_overdrive(); Self { - extended_model, extended_family, family, + extended_model, model, stepping, display_family, @@ -188,7 +187,7 @@ impl CpuSignature { /// Detects the CPU signature from CPUID leaf 1. pub fn detect() -> Self { - #[cfg(any(dos, dos32a))] + #[cfg(dos_os)] if !has_cpuid() { use super::vendor::cyrix::Cyrix; @@ -202,7 +201,7 @@ impl CpuSignature { } } - #[cfg(dos)] + #[cfg(dos_real)] if let Some(mut reset_sig) = super::get_reset_signature() { reset_sig.source = DataSource::CpuReset; return reset_sig; @@ -255,7 +254,7 @@ impl Cpu { read_multi_leaf_str(EXT_LEAF_2, EXT_LEAF_4) } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn intel_brand_index(&self) -> Option<&'static str> { let brand_id = get_brand_id(); @@ -306,7 +305,7 @@ impl Cpu { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn cleanup_model_string(s: &str) -> String { let str = s.replace("CPU", ""); @@ -360,7 +359,7 @@ impl Cpu { } CpuBrand::Intel => { // Check the Intel model lookup table - #[cfg(not(dos))] + #[cfg(not(dos_real))] if let Some(model_name) = self.intel_brand_index() { return String::from(model_name); } @@ -476,10 +475,10 @@ impl Cpu { } }; - #[cfg(not(dos))] + #[cfg(not(dos_real))] return Self::cleanup_model_string(s); - #[cfg(dos)] + #[cfg(dos_real)] String::from(s) } @@ -527,41 +526,37 @@ impl TDetect for Cpu { /// Detects and returns comprehensive CPU information. /// /// Performs full CPU detection including architecture, microarchitecture, - /// brand string, signature, features, and topology. + /// brand string, signature, features, and topology, enriching with OS + /// information on live hardware. fn detect() -> Self { - #[cfg(not(dos_os))] - let system = { - #[cfg(not(target_os = "uefi"))] - if provider::info_source() == provider::CpuidInfoSource::DumpFile { - None - } else { - crate::common::OS::get_system_name() - } - - #[cfg(target_os = "uefi")] - { - crate::common::OS::get_system_name() - } - }; + let mut cpu = Self::detect_cpuid(); - let sig = CpuSignature::detect(); - let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); - let topology = Topology::detect(); + #[cfg(std_os)] + if provider::info_source() == provider::CpuidInfoSource::Cpu { + super::os::enrich_cpu(&mut cpu); + } - #[cfg(not(dos_os))] - let cores = if is_intel() { - let detected = Self::detect_core_types(); - if detected.len() > 1 { - detected - } else { - Self::fallback_homogeneous(&arch, &topology) - } - } else { - Self::fallback_homogeneous(&arch, &topology) - }; + #[cfg(uefi)] + super::efi::enrich_cpu(&mut cpu); #[cfg(dos_os)] - let cores = Self::fallback_homogeneous(&arch, &topology); + super::dos::enrich_cpu(&mut cpu); + + cpu + } +} + +impl Cpu { + /// Detects and returns comprehensive CPU information purely from CPUID leaves. + /// + /// This method guarantees that no operating system information (system name, + /// OS socket counts, core pinning, or dynamic timer measurement) is queried. + #[must_use] + pub fn detect_cpuid() -> Self { + let sig = CpuSignature::detect(); + let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); + let topology = Topology::detect_cpuid(); + let cores = Self::detect_cpuid_core_types(&arch, &topology); let extra = X86Data { has_cpuid: (is_cyrix() && Cyrix::can_enable_cpuid()) || has_cpuid(), @@ -578,9 +573,6 @@ impl TDetect for Cpu { }; Self { - #[cfg(not(dos_os))] - system, - #[cfg(dos_os)] system: None, vendor: String::from(extra.arch.brand_name), model: extra.arch.model.clone(), @@ -589,64 +581,31 @@ impl TDetect for Cpu { extra, } } -} - -impl Cpu { - /// Creates a single homogeneous CpuCore cluster fallback based on the package topology and architecture. - pub fn fallback_homogeneous(arch: &CpuArch, topology: &Topology) -> Vec { - let speed = Speed::detect(); - let speed_opt = if speed.base > 0 { Some(speed) } else { None }; - let cache = topology.cache; - let sockets = topology.sockets.count.max(1); - let cores_per_socket = (topology.cores.count / sockets).max(1); - let threads_per_socket = (topology.threads.count / sockets).max(1); - let name = if arch.code_name != UNK { - Some(String::from(arch.code_name)) - } else { - None - }; - alloc::vec![CpuCore { - kind: CoreType::Performance, - micro_arch: arch.micro_arch, - name, - implementer: None, - cache, - speed: speed_opt, - count: cores_per_socket, - threads: threads_per_socket, - }] - } -} -#[cfg(not(dos_os))] -impl Cpu { - /// Enumerates all logical processors to discover unique core types. - /// - /// On non-DOS systems and UEFI, targets each logical processor and reads CPUID - /// leaf 0x1A to detect core type, aggregating separate entries for - /// hybrid architectures (e.g., Intel P-cores and E-cores). - /// Falls back to a single entry for DOS or if enumeration fails. - pub fn detect_core_types() -> Vec { - use super::vendor::Intel; - - let mut cores: Vec = Vec::new(); - - fn find_or_push(cores: &mut Vec, core: CpuCore) { - if let Some(c) = cores.iter_mut().find(|c| { - c.kind == core.kind && c.micro_arch == core.micro_arch && c.name == core.name - }) { - c.count += core.count; - c.threads += core.threads; - if c.speed.is_none() && core.speed.is_some() { - c.speed = core.speed; + /// Detects core types purely from CPUID contexts (if multiple dump contexts exist), + /// or returns the single homogeneous cluster fallback. + #[must_use] + pub fn detect_cpuid_core_types(arch: &CpuArch, topology: &Topology) -> Vec { + #[cfg(std_os)] + if provider::dump_cpu_count() > 1 { + use super::vendor::Intel; + + let mut cores: Vec = Vec::new(); + + fn find_or_push(cores: &mut Vec, core: CpuCore) { + if let Some(c) = cores.iter_mut().find(|c| { + c.kind == core.kind && c.micro_arch == core.micro_arch && c.name == core.name + }) { + c.count += core.count; + c.threads += core.threads; + if c.speed.is_none() && core.speed.is_some() { + c.speed = core.speed; + } + } else { + cores.push(core); } - } else { - cores.push(core); } - } - #[cfg(not(nostd_os))] - if provider::info_source() == provider::CpuidInfoSource::DumpFile { let dump_count = provider::dump_cpu_count(); for cpu_idx in 0..dump_count { provider::set_dump_cpu(cpu_idx); @@ -673,7 +632,7 @@ impl Cpu { }; let cache = Cache::detect(); - let speed = Speed::detect(); + let speed = Speed::detect_cpuid(); let speed_opt = if speed.base > 0 { Some(speed) } else { None }; find_or_push( @@ -690,125 +649,97 @@ impl Cpu { }, ); } - } - #[cfg(not(nostd_os))] - if provider::info_source() != provider::CpuidInfoSource::DumpFile - && let Some(core_ids) = core_affinity::get_core_ids() - { - for core_id in core_ids { - core_affinity::set_for_current(core_id); - - let core_type = core_type_from_cpuid(); - let sig = CpuSignature::detect(); - let arch = CpuArch::find(&Cpu::raw_model_string(), sig, &vendor_str()); - let micro_arch = if is_intel() { - Intel::core_micro_arch(arch.micro_arch, core_type) + for c in &mut cores { + let smt = if c.kind == CoreType::Efficiency { + 1 } else { - arch.micro_arch + cpuid_threads_per_core().max(1) }; - - // Make sure we know the MicroArch before pushing to core types - if micro_arch == MicroArch::Unknown { - continue; + c.count = (c.threads / smt).max(1); + if let Some(ref mut cache) = c.cache { + cache.resolve_share_counts(c.count, c.threads, 1); } + } - let name_str = micro_arch.as_str(); - let name = if name_str != UNK { - Some(String::from(name_str)) - } else { - None - }; - - let cache = Cache::detect(); - let speed = Speed::detect(); - let speed_opt = if speed.base > 0 { Some(speed) } else { None }; - - find_or_push( - &mut cores, - CpuCore { - kind: core_type, - micro_arch, - name, - implementer: None, - cache, - speed: speed_opt, - count: 1, - threads: 1, - }, - ); + if cores.len() > 1 { + return cores; } } - #[cfg(target_os = "uefi")] - if let Some(mp) = crate::x86::efi::mp::EfiMpServices::detect() { - let proc_count = mp.processor_count(); - - for cpu_idx in 0..proc_count { - let mut core_type = CoreType::default(); - let mut sig = CpuSignature::default(); - let mut raw_model = alloc::string::String::new(); - let mut vendor = alloc::string::String::new(); - let mut speed = Speed::default(); - - mp.run_on_processor(cpu_idx, || { - core_type = core_type_from_cpuid(); - sig = CpuSignature::detect(); - raw_model = Cpu::raw_model_string(); - vendor = vendor_str(); - speed = Speed::detect(); - }); - - let arch = CpuArch::find(&raw_model, sig, &vendor); - let micro_arch = if is_intel() { - Intel::core_micro_arch(arch.micro_arch, core_type) - } else { - arch.micro_arch - }; + Self::fallback_homogeneous(arch, topology) + } - if micro_arch == MicroArch::Unknown { - continue; - } + /// Creates a single homogeneous CpuCore cluster fallback based on the package topology and architecture. + pub fn fallback_homogeneous(arch: &CpuArch, topology: &Topology) -> Vec { + let speed = topology.speed; + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + let cache = topology.cache; + let sockets = topology.sockets.count.max(1); + let cores_per_socket = (topology.cores.count / sockets).max(1); + let threads_per_socket = (topology.threads.count / sockets).max(1); + let name = if arch.code_name != UNK { + Some(String::from(arch.code_name)) + } else { + None + }; + alloc::vec![CpuCore { + kind: CoreType::Performance, + micro_arch: arch.micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: cores_per_socket, + threads: threads_per_socket, + }] + } +} - let name_str = micro_arch.as_str(); - let name = if name_str != UNK { - Some(String::from(name_str)) - } else { - None - }; +#[cfg(std_os)] +impl Cpu { + /// Detects CPU information from a `CpuDump` instance without touching any OS information. + pub fn from_dump(dump: &provider::CpuDump) -> Self { + provider::set_cpuid_provider(dump.clone()); + let cpu = Self::detect_cpuid(); + provider::reset_cpuid_provider(); + cpu + } - let cache = Cache::detect(); - let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + /// Detects CPU information from a CPUID dump file without touching any OS information. + pub fn from_dump_file>(path: P) -> Self { + let dump = provider::CpuDump::parse_file(path); + Self::from_dump(&dump) + } - find_or_push( - &mut cores, - CpuCore { - kind: core_type, - micro_arch, - name, - implementer: None, - cache, - speed: speed_opt, - count: 1, - threads: 1, - }, - ); - } + /// Detects CPU information from a CPUID dump string without touching any OS information. + pub fn from_dump_str(s: &str) -> Self { + let dump = provider::CpuDump::parse_str(s); + Self::from_dump(&dump) + } +} + +#[cfg(not(dos_os))] +impl Cpu { + /// Enumerates all logical processors to discover unique core types. + pub fn detect_core_types() -> Vec { + #[cfg(std_os)] + if provider::info_source() == provider::CpuidInfoSource::DumpFile { + let sig = CpuSignature::detect(); + let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); + let topo = Topology::detect_cpuid(); + return Self::detect_cpuid_core_types(&arch, &topo); } - for c in &mut cores { - let smt = if c.kind == CoreType::Efficiency { - 1 - } else { - cpuid_threads_per_core().max(1) - }; - c.count = (c.threads / smt).max(1); - if let Some(ref mut cache) = c.cache { - cache.resolve_share_counts(c.count, c.threads, 1); - } + #[cfg(std_os)] + { + super::os::detect_live_core_types() } - cores + #[cfg(uefi)] + { + super::efi::detect_live_core_types() + } } } diff --git a/src/x86/display.rs b/src/x86/display.rs index 96f23c47..03726148 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -2,19 +2,19 @@ use super::cpu::Cpu; use super::micro_arch::MicroArch; use super::*; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use super::cache::is_asymmetric_dual_ccd_x3d; use crate::common::{CliFlags, CpuDisplay, DataSource, TCpuDisplay, UNK}; use crate::println; use alloc::format; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use alloc::string::String; fn yes_no(b: bool) -> &'static str { if b { "Yes" } else { "No" } } -#[cfg(not(dos))] +#[cfg(not(dos_real))] impl CpuDisplay { /// Computes the number of cache instances on x86 taking SMT / APIC ID allocation into account. pub fn x86_cache_instances( @@ -281,7 +281,7 @@ impl Cpu { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn print_centaur_features(&self, flags: CliFlags, disp: &CpuDisplay) { use alloc::vec::Vec; @@ -317,7 +317,7 @@ impl Cpu { } // Centaur features list - #[cfg(not(dos))] + #[cfg(not(dos_real))] if is_centaur() { self.print_centaur_features(flags, disp); } @@ -329,10 +329,10 @@ impl Cpu { impl TCpuDisplay for Cpu { fn debug(&self) { - #[cfg(not(any(dos, dos32a)))] + #[cfg(not(dos_os))] println!("{:#?}", self); - #[cfg(dos32a)] + #[cfg(dos_ext)] { use super::is_cyrix; @@ -342,7 +342,7 @@ impl TCpuDisplay for Cpu { } } - #[cfg(dos)] + #[cfg(dos_real)] { use super::is_cyrix; @@ -394,7 +394,7 @@ impl TCpuDisplay for Cpu { fn display_table(&self, flags: CliFlags) { let disp = CpuDisplay { flags }; - #[cfg(target_os = "uefi")] + #[cfg(uefi)] { let fw = crate::x86::efi::os::detect_firmware(); let mut vendor = alloc::string::String::new(); @@ -433,7 +433,7 @@ impl TCpuDisplay for Cpu { } // Hypervisor vendor_string (brand_name) - #[cfg(not(dos))] + #[cfg(not(dos_real))] if let Some(hyp_str) = &self.hyp_vendor_str { let hyp = HypervisorBrand::from(hyp_str.as_str()); println!("{}{} ({})", disp.label("Hypervisor"), hyp_str, hyp.to_str()); @@ -472,7 +472,7 @@ impl TCpuDisplay for Cpu { self.print_topology(flags, &disp); // Cache - #[cfg(not(dos))] + #[cfg(not(dos_real))] if !self.is_hybrid() { let cache_count = |share_count: u32| -> String { CpuDisplay::x86_cache_count( @@ -538,7 +538,7 @@ impl TCpuDisplay for Cpu { if !cyrix.multiplier.is_empty() && cyrix.multiplier != "0" { println!("{}{}x", disp.sublabel("Bus Multiplier"), &cyrix.multiplier); } - #[cfg(not(any(dos, dos32a)))] + #[cfg(not(dos_os))] println!(); } } diff --git a/src/x86/dos/allocator.rs b/src/x86/dos/allocator.rs index 887615e5..b0259f9e 100644 --- a/src/x86/dos/allocator.rs +++ b/src/x86/dos/allocator.rs @@ -83,10 +83,10 @@ unsafe extern "C" { pub unsafe fn init_heap() { let heap_start = &raw mut _heap as usize; - #[cfg(dos32a)] + #[cfg(dos_ext)] let heap_size = 0x100000; // 1MB heap for DOS/32A - #[cfg(dos)] + #[cfg(dos_real)] let heap_size = 0x10000usize.saturating_sub(heap_start & 0xFFFF); unsafe { ALLOCATOR.init(heap_start, heap_size) }; diff --git a/src/x86/dos/args.rs b/src/x86/dos/args.rs index de9c1104..23881a5d 100644 --- a/src/x86/dos/args.rs +++ b/src/x86/dos/args.rs @@ -1,4 +1,4 @@ -#![cfg(any(dos, dos32a))] +#![cfg(dos_os)] // ============================================================================ // Command-line arguments parsing (DOS Real Mode & Protected Mode) // ============================================================================ @@ -21,7 +21,7 @@ impl Args { static mut TAIL_BUF: [u8; 128] = [0; 128]; -#[cfg(dos32a)] +#[cfg(dos_ext)] pub fn selector_base(selector: u16) -> Option { let mut base_high: u16 = 0; let mut base_low: u16 = 0; @@ -45,7 +45,7 @@ pub fn selector_base(selector: u16) -> Option { } } -#[cfg(dos32a)] +#[cfg(dos_ext)] pub fn psp_base() -> Option { let mut psp_val: u32 = 0; unsafe { @@ -67,7 +67,7 @@ pub fn psp_base() -> Option { } } -#[cfg(dos)] +#[cfg(dos_real)] pub fn get_args() -> Args { let mut tokens = [""; MAX_ARGS]; let mut count = 0; @@ -113,7 +113,7 @@ pub fn get_args() -> Args { Args { tokens, count } } -#[cfg(dos32a)] +#[cfg(dos_ext)] pub fn get_args() -> Args { let mut tokens = [""; MAX_ARGS]; let mut count = 0; diff --git a/src/x86/dos/cache.rs b/src/x86/dos/cache.rs index f29a5030..e25ebc85 100644 --- a/src/x86/dos/cache.rs +++ b/src/x86/dos/cache.rs @@ -1,4 +1,4 @@ -#![cfg(dos)] +#![cfg(dos_real)] use crate::common::cache::Cache; diff --git a/src/x86/dos/mod.rs b/src/x86/dos/mod.rs index b4ffa9df..cf50ebaf 100644 --- a/src/x86/dos/mod.rs +++ b/src/x86/dos/mod.rs @@ -1,8 +1,10 @@ -#![cfg(any(dos, dos32a))] +#![cfg(dos_os)] //! DOS (16-bit real mode and 32-bit protected mode) environment support for rustid. use super::vendor::cyrix::Cyrix; -use crate::common::Speed; +use crate::common::{DataSource, Speed, TopologyTier}; +use crate::x86::cpu::Cpu; +use crate::x86::{cpuid_cores_per_package, cpuid_threads_per_package}; use core::arch::asm; use core::fmt::Write; @@ -12,7 +14,7 @@ pub use allocator::init_heap; pub mod args; pub use args::*; -#[cfg(dos)] +#[cfg(dos_real)] pub mod cache; pub mod fallback; @@ -22,6 +24,52 @@ pub mod mp; pub mod speed; +/// Enriches a CPU detected via pure CPUID with live DOS hardware information +/// (MP Table multi-socket counts and calibrated PIT/TSC frequency measurement). +pub fn enrich_cpu(cpu: &mut Cpu) { + // 1. Multi-socket detection from MP Table + let mp_table = mp::MpTable::detect(); + let mp_sockets = mp_table.socket_count(); + if mp_sockets > 1 { + let sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); + cpu.extra.topology.sockets = sockets; + let cores = cpu + .extra + .topology + .cores + .count + .max(cpuid_cores_per_package() * mp_sockets); + let threads = cpu + .extra + .topology + .threads + .count + .max(cpuid_threads_per_package() * mp_sockets); + cpu.extra.topology.cores = TopologyTier::new( + cores, + DataSource::Calculated("MP Table sockets * CPUID cores"), + ); + cpu.extra.topology.threads = TopologyTier::new( + threads, + DataSource::Calculated("MP Table sockets * CPUID threads"), + ); + if let Some(ref mut cache) = cpu.extra.topology.cache { + cache.resolve_share_counts(cores, threads, mp_sockets); + } + } + + // 2. Calibrated PIT/TSC speed measurement fallback + if cpu.extra.topology.speed.base == 0 { + let s = Speed::detect(); + if s.base > 0 { + cpu.extra.topology.speed = s; + if !cpu.cores.is_empty() && cpu.cores[0].speed.is_none() { + cpu.cores[0].speed = Some(s); + } + } + } +} + /// Custom panic handler for no-std environments. /// Loops indefinitely on panic to prevent undefined behavior. #[cfg(not(test))] @@ -30,7 +78,7 @@ pub mod speed; fn panic(_info: &core::panic::PanicInfo) -> ! { use crate::println; - #[cfg(dos32a)] + #[cfg(dos_ext)] if let Some(location) = _info.location() { println!( "Panic in file '{}' at line {}:{}", @@ -42,7 +90,7 @@ fn panic(_info: &core::panic::PanicInfo) -> ! { println!("Panic for unknown reason."); } - #[cfg(dos)] + #[cfg(dos_real)] println!("Panic!"); exit(1); } @@ -84,7 +132,7 @@ macro_rules! println { /// Writes a string to the DOS console. pub fn _print_str(s: &str) { - #[cfg(dos32a)] + #[cfg(dos_ext)] { if s.is_empty() { return; @@ -97,7 +145,7 @@ pub fn _print_str(s: &str) { offset += chunk_size; } } - #[cfg(dos)] + #[cfg(dos_real)] { for &b in s.as_bytes() { printc(b); @@ -116,7 +164,7 @@ impl Write for DosWriter { } /// Outputs a single character to the DOS console using INT 21h. -#[cfg(dos)] +#[cfg(dos_real)] #[inline(always)] fn printc(ch: u8) { unsafe { @@ -131,7 +179,7 @@ fn printc(ch: u8) { } /// Writes a chunk of data to stdout using INT 21h, AH=40h (protected mode supported). -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] fn write_chunk(data: &[u8]) { let len = data.len() as u16; @@ -164,7 +212,7 @@ pub fn exit(code: u8) -> ! { } /// Reads a byte from conventional memory (Real Mode). -#[cfg(dos)] +#[cfg(dos_real)] #[inline(never)] pub fn peek_u8(seg: u16, off: u16) -> u8 { let val: u16; @@ -185,7 +233,7 @@ pub fn peek_u8(seg: u16, off: u16) -> u8 { } /// Reads a 16-bit word from conventional memory (Real Mode). -#[cfg(dos)] +#[cfg(dos_real)] #[inline(never)] pub fn peek_u16(seg: u16, off: u16) -> u16 { let val: u16; @@ -205,21 +253,21 @@ pub fn peek_u16(seg: u16, off: u16) -> u16 { } /// Reads a byte from a 32-bit linear address (Protected Mode). -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] pub fn peek_u8(addr: u32) -> u8 { unsafe { core::ptr::read_volatile(addr as *const u8) } } /// Reads a 16-bit word from a 32-bit linear address (Protected Mode). -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] pub fn peek_u16(addr: u32) -> u16 { unsafe { core::ptr::read_volatile(addr as *const u16) } } /// Reads a 32-bit dword from a 32-bit linear address (Protected Mode). -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] pub fn peek_u32(addr: u32) -> u32 { unsafe { core::ptr::read_volatile(addr as *const u32) } diff --git a/src/x86/dos/mp.rs b/src/x86/dos/mp.rs index 814101c5..4fc5b1f5 100644 --- a/src/x86/dos/mp.rs +++ b/src/x86/dos/mp.rs @@ -1,4 +1,4 @@ -#![cfg(any(dos, dos32a))] +#![cfg(dos_os)] //! MultiProcessor (MP) table detection for x86 systems. //! //! This module implements scanning and parsing of the Intel MP specification @@ -61,26 +61,26 @@ struct MpFloatingPointer { mp_feature5: u8, } -#[cfg(dos)] +#[cfg(dos_real)] #[inline(always)] fn peek_u8_so(seg: u16, off: u16) -> u8 { crate::x86::dos::peek_u8(seg, off) } -#[cfg(dos)] +#[cfg(dos_real)] #[inline(always)] fn peek_u16_so(seg: u16, off: u16) -> u16 { crate::x86::dos::peek_u16(seg, off) } -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] fn peek_u8_so(seg: u16, off: u16) -> u8 { let addr = ((seg as u32) << 4) + (off as u32); crate::x86::dos::peek_u8(addr) } -#[cfg(dos32a)] +#[cfg(dos_ext)] #[inline(always)] fn peek_u16_so(seg: u16, off: u16) -> u16 { let addr = ((seg as u32) << 4) + (off as u32); @@ -173,7 +173,11 @@ impl MpTable { } } - if processors > 0 { Some(processors) } else { None } + if processors > 0 { + Some(processors) + } else { + None + } } #[inline(never)] @@ -230,7 +234,7 @@ impl MpTable { #[inline(never)] fn get_ebda_seg() -> Option { - #[cfg(dos)] + #[cfg(dos_real)] { let mut es_val: u16 = 0; let mut flags: u16 = 1; // Set carry flag to force fallback diff --git a/src/x86/dos/speed.rs b/src/x86/dos/speed.rs index 742ea4d0..944c44b7 100644 --- a/src/x86/dos/speed.rs +++ b/src/x86/dos/speed.rs @@ -6,7 +6,7 @@ use super::*; use crate::x86::{constants, cpu::CpuSignature, has_tsc, is_386, vendor_str}; -#[cfg(dos)] +#[cfg(dos_real)] impl Speed { #[inline(never)] fn measure_frequency_tsc(t1: u16) -> u32 { @@ -192,7 +192,7 @@ impl Speed { } } -#[cfg(dos32a)] +#[cfg(dos_ext)] impl Speed { #[inline(never)] fn measure_frequency_tsc(t1: u16) -> u32 { diff --git a/src/x86/dump.rs b/src/x86/dump.rs index fe9e88b8..e7d2bd74 100644 --- a/src/x86/dump.rs +++ b/src/x86/dump.rs @@ -80,7 +80,7 @@ fn dump_leaf_maybe_subleaves(f: &mut impl Write, leaf: u32, indent: usize) { pub fn dump_cpu(f: &mut impl Write, cpu_idx: usize) { // Make sure to query each thread/cpu individually, to capture the full dump // This is especially important for hybrid cpus - #[cfg(not(nostd_os))] + #[cfg(std_os)] if let Some(core_ids) = core_affinity::get_core_ids() && let Some(core_id) = core_ids.get(cpu_idx) { diff --git a/src/x86/efi/display.rs b/src/x86/efi/display.rs index 03e73232..0c44ca6f 100644 --- a/src/x86/efi/display.rs +++ b/src/x86/efi/display.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] //! EFI display driver and graphics framebuffer rendering for rustid. use core::ffi::c_void; diff --git a/src/x86/efi/mod.rs b/src/x86/efi/mod.rs index a6e672f3..fb625937 100644 --- a/src/x86/efi/mod.rs +++ b/src/x86/efi/mod.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] //! Zero-dependency UEFI environment support for rustid. pub mod display; @@ -11,3 +11,159 @@ pub use display::*; pub use mp::*; pub use os::*; pub use smbios::*; + +use crate::common::{Cache, CoreType, DataSource, Speed, TopologyTier, UNK}; +use crate::x86::cpu::{Cpu, CpuCore, CpuSignature}; +use crate::x86::micro_arch::{CpuArch, MicroArch}; +use crate::x86::vendor::Intel; +use crate::x86::{ + core_type_from_cpuid, cpuid_cores_per_package, cpuid_threads_per_core, + cpuid_threads_per_package, is_intel, vendor_str, +}; +use alloc::string::String; +use alloc::vec::Vec; + +/// Enriches a CPU detected via pure CPUID with live UEFI firmware/hardware information +/// (SMBIOS system name, SMBIOS/MP multi-socket counts, dynamic frequency measurement, and hybrid cores). +pub fn enrich_cpu(cpu: &mut Cpu) { + // 1. SMBIOS System Name + if let Some(sys_name) = smbios::detect_smbios_system_name() { + cpu.system = Some(sys_name); + } + + // 2. Multi-socket / multi-package topology from EFI MP Services / SMBIOS + let efi_sockets = crate::x86::count::get_platform_socket_count(); + if efi_sockets.count > 1 { + cpu.extra.topology.sockets = efi_sockets; + let cores = cpu + .extra + .topology + .cores + .count + .max(cpuid_cores_per_package() * efi_sockets.count); + let threads = cpu + .extra + .topology + .threads + .count + .max(cpuid_threads_per_package() * efi_sockets.count); + cpu.extra.topology.cores = + TopologyTier::new(cores, DataSource::Calculated("EFI sockets * CPUID cores")); + cpu.extra.topology.threads = TopologyTier::new( + threads, + DataSource::Calculated("EFI sockets * CPUID threads"), + ); + let sockets = cpu.extra.topology.sockets.count; + if let Some(ref mut cache) = cpu.extra.topology.cache { + cache.resolve_share_counts(cores, threads, sockets); + } + } + + // 3. Frequency measurement (TSC stall or SMBIOS fallback) + if cpu.extra.topology.speed.base == 0 { + let measured = Speed::detect(); + if measured.base > 0 { + cpu.extra.topology.speed = measured; + if cpu.cores.len() == 1 && cpu.cores[0].speed.is_none() { + cpu.cores[0].speed = Some(measured); + } + } + } + + // 4. Hybrid core discovery via EFI MP Services + if is_intel() { + let detected = detect_live_core_types(); + if detected.len() > 1 { + cpu.cores = detected; + } + } +} + +/// Enumerates all logical processors across APs in UEFI to discover unique core types (e.g. Intel P-cores and E-cores). +pub fn detect_live_core_types() -> Vec { + let mut cores: Vec = Vec::new(); + + fn find_or_push(cores: &mut Vec, core: CpuCore) { + if let Some(c) = cores + .iter_mut() + .find(|c| c.kind == core.kind && c.micro_arch == core.micro_arch && c.name == core.name) + { + c.count += core.count; + c.threads += core.threads; + if c.speed.is_none() && core.speed.is_some() { + c.speed = core.speed; + } + } else { + cores.push(core); + } + } + + if let Some(mp) = mp::EfiMpServices::detect() { + let proc_count = mp.processor_count(); + + for cpu_idx in 0..proc_count { + let mut core_type = CoreType::default(); + let mut sig = CpuSignature::default(); + let mut raw_model = String::new(); + let mut vendor = String::new(); + let mut speed = Speed::default(); + + mp.run_on_processor(cpu_idx, || { + core_type = core_type_from_cpuid(); + sig = CpuSignature::detect(); + raw_model = Cpu::raw_model_string(); + vendor = vendor_str(); + speed = Speed::detect(); + }); + + let arch = CpuArch::find(&raw_model, sig, &vendor); + let micro_arch = if is_intel() { + Intel::core_micro_arch(arch.micro_arch, core_type) + } else { + arch.micro_arch + }; + + if micro_arch == MicroArch::Unknown { + continue; + } + + let name_str = micro_arch.as_str(); + let name = if name_str != UNK { + Some(String::from(name_str)) + } else { + None + }; + + let cache = Cache::detect(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + + find_or_push( + &mut cores, + CpuCore { + kind: core_type, + micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: 1, + threads: 1, + }, + ); + } + } + + for c in &mut cores { + let smt = if c.kind == CoreType::Efficiency { + 1 + } else { + cpuid_threads_per_core().max(1) + }; + c.count = (c.threads / smt).max(1); + if let Some(ref mut cache) = c.cache { + cache.resolve_share_counts(c.count, c.threads, 1); + } + } + + cores +} diff --git a/src/x86/efi/mp.rs b/src/x86/efi/mp.rs index 24292337..fd28b726 100644 --- a/src/x86/efi/mp.rs +++ b/src/x86/efi/mp.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] //! EFI MP (MultiProcessor) Services Protocol implementation for core enumeration and targeted execution. use core::ffi::c_void; diff --git a/src/x86/efi/os.rs b/src/x86/efi/os.rs index 40a12895..486da3bc 100644 --- a/src/x86/efi/os.rs +++ b/src/x86/efi/os.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] //! Core EFI operating system bindings and services for rustid. use core::alloc::{GlobalAlloc, Layout}; @@ -488,7 +488,7 @@ unsafe impl GlobalAlloc for EfiAllocator { } } -#[cfg(target_os = "uefi")] +#[cfg(uefi)] #[global_allocator] static ALLOCATOR: EfiAllocator = EfiAllocator; diff --git a/src/x86/efi/smbios.rs b/src/x86/efi/smbios.rs index 17984b30..b6bf57f3 100644 --- a/src/x86/efi/smbios.rs +++ b/src/x86/efi/smbios.rs @@ -1,4 +1,4 @@ -#![cfg(target_os = "uefi")] +#![cfg(uefi)] //! Zero-dependency SMBIOS 2.x and 3.x parser for UEFI environment. use alloc::string::{String, ToString}; @@ -6,7 +6,7 @@ use alloc::vec::Vec; use crate::common::os::{is_generic_value, is_known_hypervisor_vendor}; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] use super::os::{EfiConfigurationTable, get_system_table}; /// SMBIOS 2.x 32-bit Table GUID: `{eb9d2d31-2d88-11d3-9a16-0090273fc14d}` @@ -495,7 +495,7 @@ impl SmbiosData { } /// Locates and parses the SMBIOS table in a live UEFI environment. -#[cfg(target_os = "uefi")] +#[cfg(uefi)] pub fn detect_smbios() -> Option { let st = get_system_table(); if st.is_null() { @@ -550,7 +550,7 @@ pub fn detect_smbios() -> Option { /// Discovers the system name from SMBIOS in UEFI. pub fn detect_smbios_system_name() -> Option { - #[cfg(target_os = "uefi")] + #[cfg(uefi)] if let Some(smbios) = detect_smbios() { return smbios.get_system_name(); } diff --git a/src/x86/features.rs b/src/x86/features.rs index 0bd4d77f..fc9e6d39 100644 --- a/src/x86/features.rs +++ b/src/x86/features.rs @@ -1,11 +1,11 @@ use super::CpuBrand; use super::constants::{EXT_LEAF_1, LEAF_1, LEAF_7}; use super::fns::{is_amd, is_cyrix, is_valid_leaf, x86_cpuid}; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use alloc::collections::BTreeMap; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use alloc::string::String; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use alloc::vec::Vec; /// CPUID register selector for feature bit checking. @@ -379,14 +379,14 @@ pub fn has_3dnow() -> bool { pub type FeatureFn = fn() -> bool; -#[cfg(not(dos))] +#[cfg(not(dos_real))] type FeatureMap<'a> = &'a [(&'static str, FeatureFn)]; -#[cfg(dos)] +#[cfg(dos_real)] pub use super::dos::dos_feature_list as get_feature_list; /// Get the full list of detected features. -#[cfg(not(dos))] +#[cfg(not(dos_real))] #[must_use] pub fn get_feature_list() -> BTreeMap<&'static str, String> { const BASIC_FEATURES: FeatureMap = &[ diff --git a/src/x86/fns.rs b/src/x86/fns.rs index 130ca15a..daded3e7 100644 --- a/src/x86/fns.rs +++ b/src/x86/fns.rs @@ -68,11 +68,11 @@ pub fn x86_cpuid_count(leaf: u32, sub_leaf: u32) -> Cpuid { #[cfg(nostd_os)] return real_x86_cpuid_count(leaf, sub_leaf); - #[cfg(not(nostd_os))] + #[cfg(std_os)] super::provider::cpuid_count(leaf, sub_leaf) } -#[cfg(not(nostd_os))] +#[cfg(std_os)] #[must_use] pub fn info_source() -> super::provider::CpuidInfoSource { super::provider::info_source() @@ -85,7 +85,7 @@ pub fn cpuid_data_source() -> DataSource { #[cfg(nostd_os)] return DataSource::Cpuid; - #[cfg(not(nostd_os))] + #[cfg(std_os)] match info_source() { super::provider::CpuidInfoSource::Cpu => DataSource::Cpuid, super::provider::CpuidInfoSource::DumpFile => DataSource::CpuidDump, @@ -347,17 +347,17 @@ pub fn amd_logical_cores() -> u32 { } /// Returns the number of physical cores per package for Intel CPUs. -#[cfg(dos)] +#[cfg(dos_real)] pub use super::dos::{ dos_cores_per_package as cpuid_cores_per_package, dos_threads_per_core as cpuid_threads_per_core, dos_threads_per_package as cpuid_threads_per_package, }; -#[cfg(not(dos))] +#[cfg(not(dos_real))] pub use cpuid_counts::*; -#[cfg(not(dos))] +#[cfg(not(dos_real))] mod cpuid_counts { use super::*; diff --git a/src/x86/mod.rs b/src/x86/mod.rs index 88710de9..7bdea81c 100644 --- a/src/x86/mod.rs +++ b/src/x86/mod.rs @@ -10,7 +10,7 @@ compile_error!("This crate only supports x86 and x86_64 architectures."); pub mod brand; -#[cfg(not(dos))] +#[cfg(not(dos_real))] pub mod cache; pub mod constants; @@ -18,10 +18,10 @@ pub mod count; pub mod cpu; pub mod display; -#[cfg(any(dos, dos32a))] +#[cfg(dos_os)] pub mod dos; -#[cfg(target_os = "uefi")] +#[cfg(uefi)] pub mod efi; pub mod dump; @@ -29,7 +29,10 @@ pub mod features; pub mod fns; pub mod micro_arch; -#[cfg(not(nostd_os))] +#[cfg(std_os)] +pub mod os; + +#[cfg(std_os)] pub mod provider; pub mod topology; diff --git a/src/x86/os/mod.rs b/src/x86/os/mod.rs new file mode 100644 index 00000000..55ebab3e --- /dev/null +++ b/src/x86/os/mod.rs @@ -0,0 +1,233 @@ +//! OS and platform-specific data gathering for x86 processors. +//! +//! This module encapsulates all host OS and platform queries: +//! - Host system / machine name (from DMI, sysfs, registry, sysctl, Haiku) +//! - Platform multi-socket counts (from /proc/cpuinfo, sysfs, Windows registry) +//! - Live core enumeration via thread affinity (`core_affinity`) +//! - Dynamic TSC frequency measurement via OS timer + +use super::cpu::{Cpu, CpuCore, CpuSignature}; +use super::micro_arch::{CpuArch, MicroArch}; +use super::vendor::Intel; +use super::{ + core_type_from_cpuid, cpuid_cores_per_package, cpuid_threads_per_core, + cpuid_threads_per_package, is_intel, vendor_str, +}; +use crate::common::{Cache, CoreType, DataSource, Speed, TopologyTier, UNK}; +use alloc::string::String; +use alloc::vec::Vec; + +#[cfg(not(dos_os))] +use crate::common::{OS, TOSData}; + +/// Returns the host system name reported by the operating system. +#[must_use] +pub fn get_system_name() -> Option { + #[cfg(not(dos_os))] + { + OS::get_system_name() + } + + #[cfg(dos_os)] + { + None + } +} + +/// Returns the number of physical sockets reported by the operating system. +#[must_use] +pub fn get_socket_count() -> TopologyTier { + #[cfg(not(dos_os))] + { + OS::get_socket_count() + } + + #[cfg(dos_os)] + { + TopologyTier::default() + } +} + +/// Dynamically measures the CPU clock frequency using RDTSC and OS timer. +#[must_use] +pub fn measure_frequency() -> Speed { + if !super::has_tsc() { + return Speed::default(); + } + + let freq = measure_frequency_tsc(); + if freq == 0 { + return Speed::default(); + } + + Speed { + base: freq, + boost: freq, + measured: true, + } +} + +fn measure_frequency_tsc() -> u32 { + #[cfg(target_arch = "x86")] + use core::arch::x86::_rdtsc as rdtsc; + #[cfg(target_arch = "x86_64")] + use core::arch::x86_64::_rdtsc as rdtsc; + + const MHZ_DIVISOR: u64 = 1_000_000; + + use core::time::Duration; + + let start_tsc = unsafe { rdtsc() }; + let start_time = std::time::Instant::now(); + + let end_time = start_time + Duration::from_millis(10); + + while std::time::Instant::now() < end_time { + core::hint::spin_loop(); + } + + let end_tsc = unsafe { rdtsc() }; + + let elapsed = start_time.elapsed().as_nanos() as u64; + let tsc_delta = end_tsc - start_tsc; + + if elapsed == 0 { + return 0; + } + + let freq_mhz = (tsc_delta * MHZ_DIVISOR) / elapsed; + + (freq_mhz / 1000) as u32 +} + +/// Discovers hybrid core types by pinning the current thread to each logical core. +#[must_use] +pub fn detect_live_core_types() -> Vec { + let mut cores: Vec = Vec::new(); + + fn find_or_push(cores: &mut Vec, core: CpuCore) { + if let Some(c) = cores + .iter_mut() + .find(|c| c.kind == core.kind && c.micro_arch == core.micro_arch && c.name == core.name) + { + c.count += core.count; + c.threads += core.threads; + if c.speed.is_none() && core.speed.is_some() { + c.speed = core.speed; + } + } else { + cores.push(core); + } + } + + if let Some(core_ids) = core_affinity::get_core_ids() { + for core_id in core_ids { + core_affinity::set_for_current(core_id); + + let core_type = core_type_from_cpuid(); + let sig = CpuSignature::detect(); + let arch = CpuArch::find(&Cpu::raw_model_string(), sig, &vendor_str()); + let micro_arch = if is_intel() { + Intel::core_micro_arch(arch.micro_arch, core_type) + } else { + arch.micro_arch + }; + + if micro_arch == MicroArch::Unknown { + continue; + } + + let name_str = micro_arch.as_str(); + let name = if name_str != UNK { + Some(String::from(name_str)) + } else { + None + }; + + let cache = Cache::detect(); + let speed = Speed::detect_cpuid(); + let speed_opt = if speed.base > 0 { Some(speed) } else { None }; + + find_or_push( + &mut cores, + CpuCore { + kind: core_type, + micro_arch, + name, + implementer: None, + cache, + speed: speed_opt, + count: 1, + threads: 1, + }, + ); + } + } + + for c in &mut cores { + let smt = if c.kind == CoreType::Efficiency { + 1 + } else { + cpuid_threads_per_core().max(1) + }; + c.count = (c.threads / smt).max(1); + if let Some(ref mut cache) = c.cache { + cache.resolve_share_counts(c.count, c.threads, 1); + } + } + + cores +} + +/// Enriches a pure CPUID detection result with host operating system information. +pub fn enrich_cpu(cpu: &mut Cpu) { + // 1. Host system / machine name + cpu.system = get_system_name(); + + // 2. Physical platform sockets + let os_sockets = get_socket_count(); + if os_sockets.count > 1 { + cpu.topology.sockets = os_sockets; + let cores = cpu + .topology + .cores + .count + .max(cpuid_cores_per_package() * os_sockets.count); + let threads = cpu + .topology + .threads + .count + .max(cpuid_threads_per_package() * os_sockets.count); + cpu.topology.cores = + TopologyTier::new(cores, DataSource::Calculated("OS sockets * CPUID cores")); + cpu.topology.threads = TopologyTier::new( + threads, + DataSource::Calculated("OS sockets * CPUID threads"), + ); + let sockets = cpu.topology.sockets.count; + if let Some(ref mut cache) = cpu.topology.cache { + cache.resolve_share_counts(cores, threads, sockets); + } + } + + // 3. Dynamic speed measurement if CPUID did not report frequency + if cpu.topology.speed.base == 0 { + let measured = measure_frequency(); + if measured.base > 0 { + cpu.topology.speed = measured; + for core in &mut cpu.cores { + if core.speed.is_none() { + core.speed = Some(measured); + } + } + } + } + + // 4. Hybrid core discovery via thread pinning on Intel processors + if is_intel() { + let live_cores = detect_live_core_types(); + if live_cores.len() > 1 { + cpu.cores = live_cores; + } + } +} diff --git a/src/x86/provider.rs b/src/x86/provider.rs index b747d4ee..3c740525 100644 --- a/src/x86/provider.rs +++ b/src/x86/provider.rs @@ -132,6 +132,10 @@ impl Default for CpuDump { impl CpuDump { pub fn parse_file>(path: P) -> Self { let contents = fs::read_to_string(path).expect("Failed to read dump file"); + Self::parse_str(&contents) + } + + pub fn parse_str(contents: &str) -> Self { let mut cpus: Vec> = Vec::new(); let mut current: Option> = None; diff --git a/src/x86/quirks.rs b/src/x86/quirks.rs index 8e1d42e6..33178be6 100644 --- a/src/x86/quirks.rs +++ b/src/x86/quirks.rs @@ -11,7 +11,7 @@ pub fn get_vendor_by_quirk() -> &'static str { return VENDOR_CYRIX; } - #[cfg(dos)] + #[cfg(dos_real)] return match get_reset_signature() { Some(signature) => match (signature.family, signature.model, signature.stepping) { // Intel RapidCAD @@ -28,7 +28,7 @@ pub fn get_vendor_by_quirk() -> &'static str { None => UNK, }; - #[cfg(not(dos))] + #[cfg(not(dos_real))] UNK } @@ -120,7 +120,7 @@ pub fn has_cyrix_5_2_quirk() -> bool { /// subsequent return to the code. /// /// Verified on some real 386/486 systems. -#[cfg(dos)] +#[cfg(dos_real)] #[allow(static_mut_refs)] pub fn get_reset_signature() -> Option { if has_cpuid() { @@ -243,7 +243,7 @@ pub fn get_reset_signature() -> Option { )) } -#[cfg(any(feature = "debug", dos))] +#[cfg(any(feature = "debug", dos_real))] pub fn debug_quirks() { use crate::println; diff --git a/src/x86/topology.rs b/src/x86/topology.rs index b53ba14d..aba4b12a 100644 --- a/src/x86/topology.rs +++ b/src/x86/topology.rs @@ -1,21 +1,23 @@ use super::constants::*; -use super::{is_valid_leaf, vendor_str, x86_cpuid_count}; +use super::{cpuid_data_source, is_valid_leaf, vendor_str, x86_cpuid_count}; use crate::common::{Cache, DataSource, Speed, TopologyTier}; -use crate::x86::count::{get_core_count, get_platform_socket_count, get_thread_count}; +use crate::x86::{cpuid_cores_per_package, cpuid_threads_per_package}; use alloc::vec::Vec; -#[cfg(not(nostd_os))] +#[cfg(std_os)] use super::{info_source, provider::CpuidInfoSource}; impl Speed { - /// Detects the CPU speed from available sources. + /// Detects CPU speed purely from CPUID leaves (Intel Leaf 16 or Transmeta Leaf 0x80860001). + /// + /// Returns default (0 MHz, unmeasured) if frequency is not reported in CPUID. #[must_use] - pub fn detect() -> Self { + pub fn detect_cpuid() -> Self { use super::{LEAF_16, x86_cpuid}; match &*vendor_str() { VENDOR_INTEL => { if !is_valid_leaf(LEAF_16) { - return Speed::measure(); + return Speed::default(); } let res = x86_cpuid(LEAF_16); @@ -24,7 +26,7 @@ impl Speed { let boost = res.ebx; if base == 0 { - return Speed::measure(); + return Speed::default(); } Speed { @@ -37,7 +39,7 @@ impl Speed { use crate::x86::TRANSMETA_LEAF_1; if !is_valid_leaf(TRANSMETA_LEAF_1) { - return Speed::measure(); + return Speed::default(); } let res = x86_cpuid(TRANSMETA_LEAF_1); @@ -50,24 +52,55 @@ impl Speed { measured: false, } } - _ => Speed::measure(), + _ => Speed::default(), } } - fn measure() -> Self { - #[cfg(not(nostd_os))] - if info_source() == CpuidInfoSource::DumpFile || !super::has_tsc() { - return Speed::default(); + /// Detects the CPU speed from available sources. + #[must_use] + pub fn detect() -> Self { + let speed = Self::detect_cpuid(); + if speed.base > 0 { + return speed; + } + + #[cfg(std_os)] + { + if info_source() == CpuidInfoSource::Cpu { + super::os::measure_frequency() + } else { + Speed::default() + } } - #[cfg(target_os = "uefi")] + #[cfg(uefi)] + { + Self::measure_uefi() + } + + #[cfg(dos_os)] + { + let freq = Self::measure_frequency(); + if freq > 0 { + Speed { + base: freq, + boost: freq, + measured: true, + } + } else { + Speed::default() + } + } + } + + #[cfg(uefi)] + fn measure_uefi() -> Self { if !super::has_tsc() { return Speed::default(); } - let freq = Self::measure_frequency(); + let freq = Self::measure_frequency_uefi(); if freq == 0 { - #[cfg(target_os = "uefi")] if let Some(smbios) = crate::x86::efi::smbios::detect_smbios() { if let Some(proc) = smbios.processors.first() { let speed = if proc.current_speed_mhz > 0 { @@ -97,42 +130,8 @@ impl Speed { } } - #[cfg(not(nostd_os))] - fn measure_frequency() -> u32 { - #[cfg(target_arch = "x86")] - use core::arch::x86::_rdtsc as rdtsc; - #[cfg(target_arch = "x86_64")] - use core::arch::x86_64::_rdtsc as rdtsc; - - const MHZ_DIVISOR: u64 = 1_000_000; - - use core::time::Duration; - - let start_tsc = unsafe { rdtsc() }; - let start_time = std::time::Instant::now(); - - let end_time = start_time + Duration::from_millis(10); - - while std::time::Instant::now() < end_time { - core::hint::spin_loop(); - } - - let end_tsc = unsafe { rdtsc() }; - - let elapsed = start_time.elapsed().as_nanos() as u64; - let tsc_delta = end_tsc - start_tsc; - - if elapsed == 0 { - return 0; - } - - let freq_mhz = (tsc_delta * MHZ_DIVISOR) / elapsed; - - (freq_mhz / 1000) as u32 - } - - #[cfg(target_os = "uefi")] - fn measure_frequency() -> u32 { + #[cfg(uefi)] + fn measure_frequency_uefi() -> u32 { #[cfg(target_arch = "x86")] use core::arch::x86::_rdtsc as rdtsc; #[cfg(target_arch = "x86_64")] @@ -212,13 +211,13 @@ pub struct Topology { } impl Topology { - /// Detects and returns the CPU topology. + /// Detects CPU topology purely from CPUID leaves without touching OS information. #[must_use] - pub fn detect() -> Self { - let speed = Speed::detect(); + pub fn detect_cpuid() -> Self { + let speed = Speed::detect_cpuid(); let mut cache = Cache::detect(); let domains: DomainList = Self::detect_domains(); - let (sockets, cores, threads) = Self::count_domains(&domains); + let (sockets, cores, threads) = Self::count_cpuid_domains(&domains); if let Some(c) = &mut cache { c.resolve_share_counts(cores.count, threads.count, sockets.count); @@ -256,33 +255,122 @@ impl Topology { } } - /// Returns (sockets, total_cores, total_threads) - fn count_domains(domains: &DomainList) -> (TopologyTier, TopologyTier, TopologyTier) { - // 1. Get raw counts from fallback sources - let sockets = if domains.is_empty() { - get_platform_socket_count() - } else { - TopologyTier::default() - }; - let threads = get_thread_count(); - let cores = get_core_count(); + /// Detects and returns the CPU topology, enriching with OS information on live hardware. + #[must_use] + pub fn detect() -> Self { + let mut topo = Self::detect_cpuid(); + + #[cfg(std_os)] + if info_source() == CpuidInfoSource::Cpu { + let os_sockets = super::os::get_socket_count(); + if os_sockets.count > 1 { + topo.sockets = os_sockets; + topo.cores = TopologyTier::new( + topo.cores + .count + .max(cpuid_cores_per_package() * os_sockets.count), + DataSource::Calculated("OS sockets * CPUID cores"), + ); + topo.threads = TopologyTier::new( + topo.threads + .count + .max(cpuid_threads_per_package() * os_sockets.count), + DataSource::Calculated("OS sockets * CPUID threads"), + ); + if let Some(c) = &mut topo.cache { + c.resolve_share_counts( + topo.cores.count, + topo.threads.count, + topo.sockets.count, + ); + } + } - if domains.is_empty() { - let total_cores = cores - .count - .max(crate::x86::cpuid_cores_per_package() * sockets.count); - let total_threads = threads - .count - .max(crate::x86::cpuid_threads_per_package() * sockets.count); + if topo.speed.base == 0 { + let measured = super::os::measure_frequency(); + if measured.base > 0 { + topo.speed = measured; + } + } + } + #[cfg(uefi)] + { + let os_sockets = crate::x86::count::get_platform_socket_count(); + if os_sockets.count > 1 { + topo.sockets = os_sockets; + topo.cores = TopologyTier::new( + topo.cores + .count + .max(cpuid_cores_per_package() * os_sockets.count), + DataSource::Calculated("EFI sockets * CPUID cores"), + ); + topo.threads = TopologyTier::new( + topo.threads + .count + .max(cpuid_threads_per_package() * os_sockets.count), + DataSource::Calculated("EFI sockets * CPUID threads"), + ); + if let Some(c) = &mut topo.cache { + c.resolve_share_counts( + topo.cores.count, + topo.threads.count, + topo.sockets.count, + ); + } + } + if topo.speed.base == 0 { + let measured = Speed::measure_uefi(); + if measured.base > 0 { + topo.speed = measured; + } + } + } + + #[cfg(dos_os)] + { + let mp_sockets = crate::x86::dos::mp::MpTable::detect().socket_count(); + if mp_sockets > 1 { + topo.sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); + topo.cores = TopologyTier::new( + topo.cores.count.max(cpuid_cores_per_package() * mp_sockets), + DataSource::Calculated("MP Table sockets * CPUID cores"), + ); + topo.threads = TopologyTier::new( + topo.threads + .count + .max(cpuid_threads_per_package() * mp_sockets), + DataSource::Calculated("MP Table sockets * CPUID threads"), + ); + if let Some(c) = &mut topo.cache { + c.resolve_share_counts(topo.cores.count, topo.threads.count, mp_sockets); + } + } + if topo.speed.base == 0 { + let measured = Speed::detect(); + if measured.base > 0 { + topo.speed = measured; + } + } + } + + topo + } + + /// Returns (sockets, total_cores, total_threads) from pure CPUID queries + fn count_cpuid_domains(domains: &DomainList) -> (TopologyTier, TopologyTier, TopologyTier) { + let sockets = TopologyTier::new(1, cpuid_data_source()); + let threads = cpuid_threads_per_package(); + let cores = cpuid_cores_per_package(); + + if domains.is_empty() { return ( sockets, - TopologyTier::new(total_cores, cores.source), - TopologyTier::new(total_threads, threads.source), + TopologyTier::new(cores.max(1), cpuid_data_source()), + TopologyTier::new(threads.max(1), cpuid_data_source()), ); } - // 2. Extract domain counts let mut threads_per_core = 1; let mut threads_per_package = 0; @@ -296,7 +384,7 @@ impl Topology { } if threads_per_package == 0 { - threads_per_package = threads.count; + threads_per_package = threads; } let t_per_core = threads_per_core.max(1); @@ -305,8 +393,8 @@ impl Topology { ( sockets, - TopologyTier::new(c_per_pkg * sockets.count, DataSource::Calculated("Cpuid")), - TopologyTier::new(t_per_pkg * sockets.count, DataSource::Calculated("Cpuid")), + TopologyTier::new(c_per_pkg.max(1), DataSource::Calculated("Cpuid")), + TopologyTier::new(t_per_pkg.max(1), DataSource::Calculated("Cpuid")), ) } diff --git a/src/x86/vendor/amd.rs b/src/x86/vendor/amd.rs index 1ceba80d..79d1560c 100644 --- a/src/x86/vendor/amd.rs +++ b/src/x86/vendor/amd.rs @@ -27,7 +27,7 @@ //! - InstLatx64 CPUID dumps & WikiChip AMD CPUID tables. use crate::x86::CpuSignature; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use crate::x86::amd_logical_cores; use crate::x86::constants::*; use crate::x86::micro_arch::{CpuArch, MicroArch}; @@ -64,7 +64,7 @@ impl Amd { Some(arch) } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn modern_micro_arch( model: &str, s: CpuSignature, @@ -182,7 +182,7 @@ impl Amd { Some(arch) } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_k8( m_lower: &str, s: CpuSignature, @@ -281,7 +281,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_k10( m_lower: &str, s: CpuSignature, @@ -359,7 +359,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_fam15h( m_lower: &str, s: CpuSignature, @@ -404,7 +404,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_fam16h( m_lower: &str, s: CpuSignature, @@ -440,7 +440,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_fam17h( m_lower: &str, s: CpuSignature, @@ -532,7 +532,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_fam19h( m_lower: &str, s: CpuSignature, @@ -686,7 +686,7 @@ impl Amd { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_fam1ah( m_lower: &str, s: CpuSignature, @@ -730,7 +730,7 @@ impl TMicroArch for Amd { return arch; } - #[cfg(not(dos))] + #[cfg(not(dos_real))] if let Some(arch) = Self::modern_micro_arch(model, s, &brand_arch) { return arch; } diff --git a/src/x86/vendor/centaur.rs b/src/x86/vendor/centaur.rs index 72881a2e..dd328387 100644 --- a/src/x86/vendor/centaur.rs +++ b/src/x86/vendor/centaur.rs @@ -4,7 +4,7 @@ use crate::x86::micro_arch::{CpuArch, MicroArch}; use crate::x86::vendor::TMicroArch; use crate::x86::{CpuSignature, is_valid_leaf, is_zhaoxin, x86_cpuid}; -#[cfg(not(dos))] +#[cfg(not(dos_real))] use alloc::vec::Vec; pub struct Centaur; @@ -23,7 +23,7 @@ fn centaur_cpu_brand() -> CpuBrand { } impl TMicroArch for Centaur { - #[cfg(dos)] + #[cfg(dos_real)] fn micro_arch(model: &str, _s: CpuSignature) -> CpuArch { let brand = centaur_cpu_brand(); let brand_arch = CpuArch::brand_arch(model, brand.to_brand_name(), VENDOR_CENTAUR); @@ -31,7 +31,7 @@ impl TMicroArch for Centaur { brand_arch(MicroArch::Unknown, UNK, None) } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn micro_arch(model: &str, s: CpuSignature) -> CpuArch { let brand = centaur_cpu_brand(); let brand_arch = CpuArch::brand_arch(model, brand.to_brand_name(), VENDOR_CENTAUR); @@ -214,7 +214,7 @@ pub type CentaurFeatureMap<'a> = &'a [( crate::x86::features::FeatureFn, )]; -#[cfg(not(dos))] +#[cfg(not(dos_real))] impl Centaur { pub fn get_feature_list() -> Vec<(&'static str, bool)> { const CENTAUR_FEATURES: CentaurFeatureMap = &[ diff --git a/src/x86/vendor/cyrix.rs b/src/x86/vendor/cyrix.rs index 9ef2e823..d8702d89 100644 --- a/src/x86/vendor/cyrix.rs +++ b/src/x86/vendor/cyrix.rs @@ -165,12 +165,12 @@ impl Cyrix { } } - #[cfg(all(not(dos), not(dos32a)))] + #[cfg(not(dos_os))] fn get_device_ids() -> (u8, u8) { (Self::get_device_id_from_signature(), 0) } - #[cfg(all(not(dos), not(dos32a)))] + #[cfg(not(dos_os))] fn get_device_id_from_signature() -> u8 { let sig = CpuSignature::detect(); @@ -230,7 +230,7 @@ impl Cyrix { fn get_device_ids() -> (u8, u8) { let (dir0, dir1) = Self::get_raw_device_ids(); - #[cfg(dos)] + #[cfg(dos_real)] let dir0 = if dir0 == 0xFF || dir0 == 0x00 { Self::get_device_id_from_signature() } else { @@ -240,7 +240,7 @@ impl Cyrix { (dir0, dir1) } - #[cfg(dos)] + #[cfg(dos_real)] fn get_device_id_from_signature() -> u8 { if let Some(signature) = crate::x86::get_reset_signature() { return match (signature.family, signature.model) { diff --git a/src/x86/vendor/intel.rs b/src/x86/vendor/intel.rs index 11b03083..01419e2b 100644 --- a/src/x86/vendor/intel.rs +++ b/src/x86/vendor/intel.rs @@ -1,4 +1,4 @@ -#[cfg(any(not(nostd_os), target_os = "uefi"))] +#[cfg(not(dos_real))] use crate::common::CoreType; use crate::x86::CpuSignature; use crate::x86::constants::*; @@ -45,7 +45,7 @@ impl Intel { Some(arch) } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_hedt_server( model: &str, ma: MicroArch, @@ -61,7 +61,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_55h( model: &str, stepping: u32, @@ -103,7 +103,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_8eh( model: &str, stepping: u32, @@ -154,7 +154,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_9eh( model: &str, stepping: u32, @@ -206,7 +206,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_8fh( model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, @@ -237,7 +237,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_b7h( model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, @@ -284,7 +284,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_0fh( model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, @@ -313,7 +313,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_17h( model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, @@ -345,7 +345,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn disambiguate_06_beh( model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, @@ -362,7 +362,7 @@ impl Intel { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn modern_micro_arch( model: &str, s: CpuSignature, @@ -602,7 +602,7 @@ impl TMicroArch for Intel { return arch; } - #[cfg(not(dos))] + #[cfg(not(dos_real))] if let Some(arch) = Self::modern_micro_arch(model, s, &brand_arch) { return arch; } @@ -611,7 +611,7 @@ impl TMicroArch for Intel { } } -#[cfg(any(not(nostd_os), target_os = "uefi"))] +#[cfg(not(dos_real))] impl Intel { pub fn core_micro_arch(parent: MicroArch, core_type: CoreType) -> MicroArch { match (parent, core_type) { diff --git a/tests/cpuid_dump_test.rs b/tests/cpuid_dump_test.rs index 85d08cab..86b6dc59 100644 --- a/tests/cpuid_dump_test.rs +++ b/tests/cpuid_dump_test.rs @@ -90,17 +90,27 @@ fn assert_brand_eq(expected: &str) { fn assert_topology(sockets: u32, cores: u32, threads: u32) { let cpu = Cpu::detect(); + assert_eq!(cpu.system, None, "Expected no OS system name in dump mode"); assert_eq!(cpu.topology.sockets.count, sockets, "Sockets mismatch"); assert_eq!(cpu.topology.cores.count, cores, "Cores mismatch"); assert_eq!(cpu.topology.threads.count, threads, "Threads mismatch"); + assert!( + !cpu.topology.speed.measured, + "Speed should not be measured via OS timer in dump mode" + ); } fn assert_topology_full(sockets: u32, dies: u32, cores: u32, threads: u32) { let cpu = Cpu::detect(); + assert_eq!(cpu.system, None, "Expected no OS system name in dump mode"); assert_eq!(cpu.topology.sockets.count, sockets, "Sockets mismatch"); assert_eq!(cpu.topology.dies.count, dies, "Dies mismatch"); assert_eq!(cpu.topology.cores.count, cores, "Cores mismatch"); assert_eq!(cpu.topology.threads.count, threads, "Threads mismatch"); + assert!( + !cpu.topology.speed.measured, + "Speed should not be measured via OS timer in dump mode" + ); } fn assert_cache_counts( @@ -1097,3 +1107,61 @@ fn test_all_vendor_strings() { assert_eq!(CpuBrand::from(vendor_str), expected_brand); } } + +#[test] +fn test_cpu_from_dump_file_helper() { + let path = raw_path("dump/eeepc.txt"); + let cpu = Cpu::from_dump_file(path); + assert_eq!(cpu.system, None); + assert!(cpu.display_model_string().contains("Celeron")); + assert_eq!(cpu.topology.sockets.count, 1); + assert_eq!(cpu.topology.cores.count, 1); + assert_eq!(cpu.topology.threads.count, 1); + assert!(!cpu.topology.speed.measured); +} + +#[test] +fn test_cpu_from_dump_str_helper() { + let raw = include_str!("cpuid/dump/eeepc.txt"); + let cpu = Cpu::from_dump_str(raw); + assert_eq!(cpu.system, None); + assert!(cpu.display_model_string().contains("Celeron")); + assert_eq!(cpu.topology.sockets.count, 1); + assert_eq!(cpu.topology.cores.count, 1); + assert_eq!(cpu.topology.threads.count, 1); + assert!(!cpu.topology.speed.measured); +} + +#[test] +fn test_cpu_from_dump_hybrid_12700h() { + let path = raw_path("dump/12700H.txt"); + let cpu = Cpu::from_dump_file(path); + assert_eq!(cpu.system, None); + assert!(cpu.is_hybrid()); + assert_eq!(cpu.cores.len(), 2); + assert_eq!(cpu.cores[0].kind, CoreType::Performance); + assert_eq!(cpu.cores[0].micro_arch, MicroArch::GoldenCove); + assert_eq!(cpu.cores[1].kind, CoreType::Efficiency); + assert_eq!(cpu.cores[1].micro_arch, MicroArch::Gracemont); + assert_eq!(cpu.topology.sockets.count, 1); + assert_eq!(cpu.topology.cores.count, 10); + assert_eq!(cpu.topology.threads.count, 20); + assert!(!cpu.topology.speed.measured); +} + +#[test] +fn test_pure_cpuid_detect_never_populates_os_data() { + let path = raw_path("dump/7950x3d.txt"); + let cpu = Cpu::from_dump_file(path); + assert_eq!(cpu.system, None); + assert_eq!(cpu.vendor, "AMD"); + assert_eq!( + cpu.display_model_string(), + "AMD Ryzen 9 7950X3D 16-Core Processor" + ); + assert_eq!(cpu.topology.sockets.count, 1); + assert_eq!(cpu.topology.dies.count, 2); + assert_eq!(cpu.topology.cores.count, 16); + assert_eq!(cpu.topology.threads.count, 32); + assert!(!cpu.topology.speed.measured); +} From ed50ece8f3adfff613cc5162785e39517aefb8ca Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 11:14:08 -0400 Subject: [PATCH 13/30] Restore dos topology fix --- src/x86/cpu.rs | 4 +-- src/x86/dos/mod.rs | 28 ++++++------------ src/x86/dos/mp.rs | 71 +++++++++++++++++++++++++++++++++++++++------ src/x86/topology.rs | 18 +++++++----- 4 files changed, 83 insertions(+), 38 deletions(-) diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index 2fcb5c4d..88efd792 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -226,8 +226,8 @@ impl CpuSignature { } } -/// x86 architecture-specific data. -#[derive(Debug, Default, PartialEq)] +/// Extended x86-specific CPU information. +#[derive(Debug, PartialEq, Default)] pub struct X86Data { /// Does this cpu have cpuid instruction support pub has_cpuid: bool, diff --git a/src/x86/dos/mod.rs b/src/x86/dos/mod.rs index cf50ebaf..eb41293c 100644 --- a/src/x86/dos/mod.rs +++ b/src/x86/dos/mod.rs @@ -4,7 +4,6 @@ use super::vendor::cyrix::Cyrix; use crate::common::{DataSource, Speed, TopologyTier}; use crate::x86::cpu::Cpu; -use crate::x86::{cpuid_cores_per_package, cpuid_threads_per_package}; use core::arch::asm; use core::fmt::Write; @@ -30,28 +29,19 @@ pub fn enrich_cpu(cpu: &mut Cpu) { // 1. Multi-socket detection from MP Table let mp_table = mp::MpTable::detect(); let mp_sockets = mp_table.socket_count(); - if mp_sockets > 1 { + let total_cores = mp_table.total_cores(); + let total_threads = mp_table.total_threads(); + + if mp_sockets > 1 || total_threads > cpu.extra.topology.threads.count { let sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); cpu.extra.topology.sockets = sockets; - let cores = cpu - .extra - .topology - .cores - .count - .max(cpuid_cores_per_package() * mp_sockets); - let threads = cpu - .extra - .topology - .threads - .count - .max(cpuid_threads_per_package() * mp_sockets); - cpu.extra.topology.cores = TopologyTier::new( - cores, - DataSource::Calculated("MP Table sockets * CPUID cores"), - ); + let cores = cpu.extra.topology.cores.count.max(total_cores); + let threads = cpu.extra.topology.threads.count.max(total_threads); + cpu.extra.topology.cores = + TopologyTier::new(cores, DataSource::Calculated("MP Table * CPUID cores")); cpu.extra.topology.threads = TopologyTier::new( threads, - DataSource::Calculated("MP Table sockets * CPUID threads"), + DataSource::Calculated("MP Table logical processors"), ); if let Some(ref mut cache) = cpu.extra.topology.cache { cache.resolve_share_counts(cores, threads, mp_sockets); diff --git a/src/x86/dos/mp.rs b/src/x86/dos/mp.rs index 4fc5b1f5..cea12aee 100644 --- a/src/x86/dos/mp.rs +++ b/src/x86/dos/mp.rs @@ -18,18 +18,33 @@ impl Default for MpTable { } impl MpTable { - /// Returns the number of enabled processors. + /// Returns the total number of enabled logical processors (threads) found in MP Table. #[must_use] pub fn processor_count(&self) -> u32 { self.processors } - /// Returns the number of processor sockets. + /// Returns the detected socket count based on total logical processors and CPUID threads per package. #[must_use] pub fn socket_count(&self) -> u32 { let threads_per_pkg = crate::x86::cpuid_threads_per_package().max(1); (self.processors / threads_per_pkg).max(1) } + + /// Returns the total physical core count across all sockets. + #[must_use] + pub fn total_cores(&self) -> u32 { + let cores_per_pkg = crate::x86::cpuid_cores_per_package().max(1); + let sockets = self.socket_count(); + cores_per_pkg * sockets + } + + /// Returns the total logical thread count across all sockets. + #[must_use] + pub fn total_threads(&self) -> u32 { + let threads_per_pkg = crate::x86::cpuid_threads_per_package().max(1); + self.processors.max(threads_per_pkg) + } } /// MP Floating Pointer Structure signature: "_MP_" @@ -92,12 +107,6 @@ impl MpTable { pub fn detect() -> MpTable { let mut table = MpTable { processors: 1 }; - // MP Table lookup is only applicable to certain CPUs - if !(crate::x86::is_intel() || crate::x86::is_vortex() || crate::x86::is_centaur()) { - return table; - } - - // Fallback: Scan memory ranges safely if let Some(mpfp) = Self::find_mpfp() { if mpfp.config_table_ptr != 0 && let Some(count) = Self::parse_config_table(mpfp.config_table_ptr) @@ -133,7 +142,7 @@ impl MpTable { return None; } - let mut buf = [0u8; 512]; + let mut buf = [0u8; 1024]; for (i, b) in buf.iter_mut().enumerate() { if (off as usize + i) > 0xFFFF { break; @@ -340,4 +349,48 @@ mod tests { data[0..4].copy_from_slice(b"INVALID"); assert_eq!(MpTable::parse_pcmp_slice(&data), None); } + + #[test] + fn test_parse_pcmp_large_table_16_processors() { + let mut data = [0u8; 512]; + data[0..4].copy_from_slice(b"PCMP"); + // 16 processor entries + 4 bus entries = 20 entries + data[34..36].copy_from_slice(&20u16.to_le_bytes()); + + for i in 0..16 { + let off = 44 + i * 20; + data[off] = 0; // Processor + data[off + 1] = i as u8; // APIC ID + data[off + 3] = 0x01; // Enabled + } + + // 4 bus entries (Type 1, 8 bytes) + for i in 0..4 { + let off = 44 + 16 * 20 + i * 8; + data[off] = 1; + } + + assert_eq!(MpTable::parse_pcmp_slice(&data), Some(16)); + } + + #[test] + fn test_mp_table_topology_calculations() { + let mp_single = MpTable { processors: 1 }; + assert_eq!(mp_single.processor_count(), 1); + assert_eq!(mp_single.socket_count(), 1); + assert_eq!(mp_single.total_cores(), 1); + assert_eq!(mp_single.total_threads(), 1); + + let mp_dual = MpTable { processors: 2 }; + assert_eq!(mp_dual.processor_count(), 2); + assert_eq!(mp_dual.socket_count(), 2); + assert_eq!(mp_dual.total_cores(), 2); + assert_eq!(mp_dual.total_threads(), 2); + + let mp_quad = MpTable { processors: 4 }; + assert_eq!(mp_quad.processor_count(), 4); + assert_eq!(mp_quad.socket_count(), 4); + assert_eq!(mp_quad.total_cores(), 4); + assert_eq!(mp_quad.total_threads(), 4); + } } diff --git a/src/x86/topology.rs b/src/x86/topology.rs index aba4b12a..c9f49901 100644 --- a/src/x86/topology.rs +++ b/src/x86/topology.rs @@ -329,18 +329,20 @@ impl Topology { #[cfg(dos_os)] { - let mp_sockets = crate::x86::dos::mp::MpTable::detect().socket_count(); - if mp_sockets > 1 { + let mp_table = crate::x86::dos::mp::MpTable::detect(); + let mp_sockets = mp_table.socket_count(); + let total_cores = mp_table.total_cores(); + let total_threads = mp_table.total_threads(); + + if mp_sockets > 1 || total_threads > topo.threads.count { topo.sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); topo.cores = TopologyTier::new( - topo.cores.count.max(cpuid_cores_per_package() * mp_sockets), - DataSource::Calculated("MP Table sockets * CPUID cores"), + topo.cores.count.max(total_cores), + DataSource::Calculated("MP Table * CPUID cores"), ); topo.threads = TopologyTier::new( - topo.threads - .count - .max(cpuid_threads_per_package() * mp_sockets), - DataSource::Calculated("MP Table sockets * CPUID threads"), + topo.threads.count.max(total_threads), + DataSource::Calculated("MP Table logical processors"), ); if let Some(c) = &mut topo.cache { c.resolve_share_counts(topo.cores.count, topo.threads.count, mp_sockets); From bce3534a67cc41b01e2aec2df01b7cfea06950a9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 11:21:30 -0400 Subject: [PATCH 14/30] Add check-all recipe that runs compile checks for several build targets --- Makefile | 34 +++++++++++++++++++++++++++++++++- justfile | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b921e8a..14a448b4 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ BASE_RUN := cargo run BASE_CHECK := cargo check --all-targets endif -.PHONY: default check check-riscv check-win-arm lint fix fmt quality build build-debug build-release _cargo_cross _build-dos-tools build-dos-real _build-dos32a-tools _build-dos32a-rustid build-dos32a build-dos build-windows build-windows-arm build-windows-gnu build-arm64 build-ppc build-mac build-mac-arm build-486 build-efi-64 build-efi-32 build-efi build-486-musl clean clean-files run from-file run-x86-emu run-dos test-dos run-efi-64 run-efi-32 test coverage test-all test-arm test-x86 +.PHONY: default check check-efi-64 check-efi-32 check-efi check-dos-real check-dos32a check-dos check-486 check-all check-riscv check-win-arm lint fix fmt quality build build-debug build-release _cargo_cross _build-dos-tools build-dos-real _build-dos32a-tools _build-dos32a-rustid build-dos32a build-dos build-windows build-windows-arm build-windows-gnu build-arm64 build-ppc build-mac build-mac-arm build-486 build-efi-64 build-efi-32 build-efi build-486-musl clean clean-files run from-file run-x86-emu run-dos test-dos run-efi-64 run-efi-32 test coverage test-all test-arm test-x86 # Lists the available actions default: @@ -40,6 +40,30 @@ endif check: $(BASE_CHECK) +# Compile check for 64-bit x86 EFI application +check-efi-64: + @if ! rustup target list --installed | grep -q x86_64-unknown-uefi; then rustup target add x86_64-unknown-uefi; fi + cargo check --target x86_64-unknown-uefi --features efi-build --bin efi_rustid + +# Compile check for 32-bit x86 EFI application +check-efi-32: + @if ! rustup target list --installed | grep -q i686-unknown-uefi; then rustup target add i686-unknown-uefi; fi + cargo check --target i686-unknown-uefi --features efi-build --bin efi_rustid + +# Compile check for both 32-bit and 64-bit EFI +check-efi: check-efi-64 check-efi-32 + +# Compile check for DOS (real-mode EXE) +check-dos-real: _build-dos-tools + @RUSTFLAGS="-C link-arg=-Tbuild-config/link-exe.x" cargo +nightly check -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos.json --release --features dos-build --bin rust86 + +# Compile check for DOS/32A (protected-mode LE) +check-dos32a: _build-dos32a-tools + @RUSTFLAGS="-C link-arg=-Tbuild-config/link-dos32a.x -C link-arg=--emit-relocs -C strip=none" cargo +nightly check -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos32a.json --features="dos32a-build" --bin dos_rustid --release + +# Compile check for all DOS targets +check-dos: check-dos32a check-dos-real + # Compile check for Risc V check-riscv: cargo check --target riscv64gc-unknown-linux-gnu @@ -48,6 +72,14 @@ check-riscv: check-win-arm: cargo check --target aarch64-pc-windows-msvc +# Compile check for 32-bit Linux 486 +check-486: + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi + cargo +nightly check -Zjson-target-spec -Z build-std=std,core,alloc,panic_abort --target build-config/i486-linux.json --release + +# Compile check for all supported targets and platforms +check-all: check check-efi check-dos check-riscv check-win-arm check-486 + # More in-depth code style checking lint: cargo clippy --all-targets --all-features diff --git a/justfile b/justfile index e44faf7d..05aeb75a 100644 --- a/justfile +++ b/justfile @@ -19,6 +19,30 @@ _cargo_cross: check: {{ base_check }} +# Compile check for 64-bit x86 EFI application +check-efi-64: + @if ! rustup target list --installed | grep -q x86_64-unknown-uefi; then rustup target add x86_64-unknown-uefi; fi + cargo check --target x86_64-unknown-uefi --features efi-build --bin efi_rustid + +# Compile check for 32-bit x86 EFI application +check-efi-32: + @if ! rustup target list --installed | grep -q i686-unknown-uefi; then rustup target add i686-unknown-uefi; fi + cargo check --target i686-unknown-uefi --features efi-build --bin efi_rustid + +# Compile check for both 32-bit and 64-bit EFI +check-efi: check-efi-64 check-efi-32 + +# Compile check for DOS (real-mode EXE) +check-dos-real: _build-dos-tools + @RUSTFLAGS="-C link-arg=-Tbuild-config/link-exe.x" cargo +nightly check -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos.json --release --features dos-build --bin rust86 + +# Compile check for DOS/32A (protected-mode LE) +check-dos32a: _build-dos32a-tools + @RUSTFLAGS="-C link-arg=-Tbuild-config/link-dos32a.x -C link-arg=--emit-relocs -C strip=none" cargo +nightly check -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos32a.json --features="dos32a-build" --bin dos_rustid --release + +# Compile check for all DOS targets +check-dos: check-dos32a check-dos-real + # Compile check for Risc V check-riscv: cargo check --target riscv64gc-unknown-linux-gnu @@ -27,6 +51,14 @@ check-riscv: check-win-arm: cargo check --target aarch64-pc-windows-msvc +# Compile check for 32-bit Linux 486 +check-486: + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi + cargo +nightly check -Zjson-target-spec -Z build-std=std,core,alloc,panic_abort --target build-config/i486-linux.json --release + +# Compile check for all supported targets and platforms +check-all: check check-efi check-dos check-riscv check-win-arm check-486 + # More in-depth code style checking lint: cargo clippy --all-targets --all-features From 8aa4936211f6df04a649d7073afc9c785ea61924 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 12:40:53 -0400 Subject: [PATCH 15/30] Clean up more compile guards --- src/arm/features.rs | 5 +---- src/arm/mod.rs | 14 ++++---------- src/arm/os/mod.rs | 10 +++++----- src/arm/os/windows.rs | 2 +- src/common/display.rs | 2 +- src/common/os/haiku.rs | 4 ++-- src/common/os/mod.rs | 2 +- src/riscv/mod.rs | 2 +- src/riscv/os/mod.rs | 8 ++++---- 9 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/arm/features.rs b/src/arm/features.rs index 5997ab50..45af3468 100644 --- a/src/arm/features.rs +++ b/src/arm/features.rs @@ -324,10 +324,7 @@ pub fn get_feature_list() -> BTreeMap<&'static str, String> { // Base detected.insert("fp", f.has_fp()); detected.insert("asimd", f.has_asimd()); - detected.insert( - "cpuid", - cfg!(any(target_os = "android", target_os = "linux")), - ); // Only on Linux/Android via HWCAP_CPUID + detected.insert("cpuid", cfg!(linux_os)); // Only on Linux/Android via HWCAP_CPUID // SIMD detected.insert("neon", f.has_neon()); diff --git a/src/arm/mod.rs b/src/arm/mod.rs index de2a5794..dc530ef1 100644 --- a/src/arm/mod.rs +++ b/src/arm/mod.rs @@ -28,16 +28,13 @@ pub trait TArmCpu { /// /// The MIDR contains information about the CPU implementer, part number, and revision. pub fn get_midr() -> usize { - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(any(windows_os, target_os = "macos"))] return get_synth_midr(); - #[cfg(not(any(target_os = "windows", target_os = "macos")))] + #[cfg(not(any(windows_os, target_os = "macos")))] { // ARMv7 and ARMv8 (AArch64) have MIDR at c0, so `mrs r0, MIDR` or `mrs x0, MIDR_EL1` - #[cfg(all( - target_arch = "arm", - not(any(target_os = "android", target_os = "linux")) - ))] + #[cfg(all(target_arch = "arm", not(linux_os)))] { let mut midr: usize = 0; // For ARMv7-A and earlier, MIDR is c0, c0, 0 @@ -56,10 +53,7 @@ pub fn get_midr() -> usize { midr } #[cfg(not(any( - all( - target_arch = "arm", - not(any(target_os = "android", target_os = "linux")) - ), + all(target_arch = "arm", not(linux_os)), target_arch = "aarch64", target_arch = "arm64ec" )))] diff --git a/src/arm/os/mod.rs b/src/arm/os/mod.rs index 607fbb96..32d2417b 100644 --- a/src/arm/os/mod.rs +++ b/src/arm/os/mod.rs @@ -25,7 +25,7 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> Vec { let runtime_cache = Cache::detect(); - #[cfg(any(target_os = "android", target_os = "linux"))] + #[cfg(linux_os)] let sysfs_per_type = Cache::from_sys_fs_per_type(); let mut core_cache_map: BTreeMap> = BTreeMap::new(); @@ -35,14 +35,14 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> Vec { unique_midrs.dedup(); for midr in &unique_midrs { - #[cfg(any(target_os = "android", target_os = "linux"))] + #[cfg(linux_os)] let cache = sysfs_per_type .as_ref() .and_then(|m| m.get(&midr.to_bits()).copied()) .or(runtime_cache) .or(None); - #[cfg(not(any(target_os = "android", target_os = "linux")))] + #[cfg(not(linux_os))] let cache = runtime_cache.or(None); core_cache_map.insert(midr.to_bits(), cache); @@ -125,9 +125,9 @@ pub use linux::*; // ! Windows // ---------------------------------------------------------------------------- -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub mod windows; -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub use windows::*; // ---------------------------------------------------------------------------- diff --git a/src/arm/os/windows.rs b/src/arm/os/windows.rs index e0352dd2..370fe2f9 100644 --- a/src/arm/os/windows.rs +++ b/src/arm/os/windows.rs @@ -242,7 +242,7 @@ pub fn get_all_features() -> BTreeMap<&'static str, String> { crate::arm::features::build_feature_map(&detected) } -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub fn get_windows_midrs() -> Vec { use std::mem::size_of; use windows::Win32::System::Registry::*; diff --git a/src/common/display.rs b/src/common/display.rs index c0985370..edeaa592 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -82,7 +82,7 @@ impl CpuDisplay { } pub fn newline(&self) { - #[cfg(not(any(dos, dos32a)))] + #[cfg(not(dos_os))] if !self.flags.compact { println!(); } diff --git a/src/common/os/haiku.rs b/src/common/os/haiku.rs index 7ea8e5b0..5f37015f 100644 --- a/src/common/os/haiku.rs +++ b/src/common/os/haiku.rs @@ -32,14 +32,14 @@ impl TOSData for OS { fn get_socket_count() -> TopologyTier { let (total_cpus, source) = cpu_count_from_sysinfo("sysinfo"); - #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + #[cfg(x86_cpu)] { let threads_per_pkg = crate::x86::cpuid_threads_per_package().max(1); let sockets = (total_cpus / threads_per_pkg).max(1); TopologyTier::new(sockets, source) } - #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] + #[cfg(not(x86_cpu))] { TopologyTier::new(1, source) } diff --git a/src/common/os/mod.rs b/src/common/os/mod.rs index 46db2bfa..523a6dcb 100644 --- a/src/common/os/mod.rs +++ b/src/common/os/mod.rs @@ -25,7 +25,7 @@ pub mod sysctl; #[cfg(any(target_os = "haiku", test))] pub mod haiku; -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub mod windows; // ---------------------------------------------------------------------------- diff --git a/src/riscv/mod.rs b/src/riscv/mod.rs index e4c84899..3443d9ca 100644 --- a/src/riscv/mod.rs +++ b/src/riscv/mod.rs @@ -1,4 +1,4 @@ -#![cfg(any(target_arch = "riscv64", test))] +#![cfg(any(riscv_cpu, test))] //! RISC-V CPU detection. pub mod brand; diff --git a/src/riscv/os/mod.rs b/src/riscv/os/mod.rs index 6ebb4b35..361a70cd 100644 --- a/src/riscv/os/mod.rs +++ b/src/riscv/os/mod.rs @@ -19,12 +19,12 @@ pub struct OsCpuInfo { // Linux // ---------------------------------------------------------------------------- -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(linux_os)] pub mod linux; -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(linux_os)] pub use linux::*; -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(linux_os))] pub mod fallback { use super::*; pub fn detect() -> OsCpuInfo { @@ -43,5 +43,5 @@ pub mod fallback { BTreeMap::new() } } -#[cfg(not(any(target_os = "android", target_os = "linux")))] +#[cfg(not(linux_os))] pub use fallback::*; From 8a5a1bc1b6e95fa42df3f14f0d04f7a09578820e Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 12:52:40 -0400 Subject: [PATCH 16/30] Run compile checks in CI --- .github/workflows/push.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 0d1e8560..4a2aaebb 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -25,6 +25,9 @@ jobs: - uses: actions/checkout@v6 + - name: Compile checks + run: just check-all + - name: Run tests run: just test-all From 0f2f4cf46237bddbff0abb07730e6846ec07a539 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 13:17:08 -0400 Subject: [PATCH 17/30] Refactor display logic to use new methods based on common patterns --- src/arm/display.rs | 101 ++++++++------------- src/common/display.rs | 186 ++++++++++++++++++++++++++++++++++----- src/common/os/android.rs | 93 -------------------- src/common/os/common.rs | 137 ++++++++++++++++++++++++++++ src/common/os/linux.rs | 110 +---------------------- src/common/os/mod.rs | 3 - src/ppc/display.rs | 4 +- src/riscv/display.rs | 19 ++-- src/x86/display.rs | 175 ++++++++++-------------------------- 9 files changed, 398 insertions(+), 430 deletions(-) diff --git a/src/arm/display.rs b/src/arm/display.rs index 133062a7..f5f210c6 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -3,15 +3,6 @@ use super::micro_arch::MicroArch; use super::*; use crate::common::{CliFlags, CpuDisplay, TCpuDisplay, UNK}; -fn is_duplicate(a: &str, b: &str) -> bool { - if a.is_empty() || b.is_empty() { - return false; - } - let a_lower = a.to_ascii_lowercase(); - let b_lower = b.to_ascii_lowercase(); - a_lower == b_lower || a_lower.contains(&b_lower) || b_lower.contains(&a_lower) -} - impl CpuDisplay { pub fn should_show_model(cpu_info: &Cpu, verbose: bool) -> bool { let model = &cpu_info.cpu_arch.model; @@ -22,23 +13,23 @@ impl CpuDisplay { return true; } if let Some(soc) = &cpu_info.soc_model - && is_duplicate(model, soc) + && Self::is_duplicate(model, soc) { return false; } let code_name = cpu_info.cpu_arch.code_name; - if code_name != UNK && !code_name.is_empty() && is_duplicate(model, code_name) { + if code_name != UNK && !code_name.is_empty() && Self::is_duplicate(model, code_name) { return false; } for core in &cpu_info.cores { let ma_str: String = core.micro_arch.into(); - if ma_str != UNK && is_duplicate(model, &ma_str) { + if ma_str != UNK && Self::is_duplicate(model, &ma_str) { return false; } if let Some(cname) = &core.name && cname != UNK && !cname.is_empty() - && is_duplicate(model, cname) + && Self::is_duplicate(model, cname) { return false; } @@ -69,7 +60,7 @@ impl CpuDisplay { break; }; let ma_str: String = core.micro_arch.into(); - if ma_str != UNK && is_duplicate(cname, &ma_str) { + if ma_str != UNK && Self::is_duplicate(cname, &ma_str) { common_cname = None; break; } @@ -110,12 +101,12 @@ impl CpuDisplay { return Some(code_name); } - if is_duplicate(code_name, &cpu_info.cpu_arch.model) { + if Self::is_duplicate(code_name, &cpu_info.cpu_arch.model) { return None; } if let Some(soc) = &cpu_info.soc_model - && is_duplicate(code_name, soc) + && Self::is_duplicate(code_name, soc) { return None; } @@ -124,7 +115,7 @@ impl CpuDisplay { let mut all_match_ma = !cpu_info.cores.is_empty(); for core in &cpu_info.cores { let ma_str: String = core.micro_arch.into(); - if ma_str == UNK || !is_duplicate(code_name, &ma_str) { + if ma_str == UNK || !Self::is_duplicate(code_name, &ma_str) { all_match_ma = false; break; } @@ -151,7 +142,7 @@ impl CpuDisplay { return true; } let ma_str: String = core.micro_arch.into(); - if ma_str != UNK && is_duplicate(code_name, &ma_str) { + if ma_str != UNK && Self::is_duplicate(code_name, &ma_str) { return false; } // When all core types share the same codename, display it only in the CPU/SoC section, not with the cores @@ -170,32 +161,24 @@ impl CpuDisplay { disp.display_system(system, flags); } - if let Some(soc_model) = &cpu_info.soc_model { - disp.simple_line("SoC", soc_model); - } + disp.simple_line_opt("SoC", cpu_info.soc_model.as_deref()); if Self::should_show_model(cpu_info, flags.verbose) { disp.simple_line("Model", &cpu_info.cpu_arch.model); } - if let Some(codename) = Self::should_show_codename(cpu_info, flags.verbose) { - disp.simple_line("Codename", codename); - } + disp.simple_line_opt( + "Codename", + Self::should_show_codename(cpu_info, flags.verbose), + ); - if let Some(tech) = cpu_info.cpu_arch.technology { - disp.simple_line("Process", tech); - } + disp.simple_line_opt("Process", cpu_info.cpu_arch.technology); if cpu_info.is_hybrid() { for (i, core) in cpu_info.cores.iter().enumerate() { - let core_num = format!("Core #{}", i + 1); - println!("{}", disp.label(&core_num)); + disp.core_heading(i); - if let Some(ref vendor_str) = core.implementer - && vendor_str != UNK - { - disp.section_line("Implementer", vendor_str); - } + disp.section_line_opt("Implementer", core.implementer.as_deref()); let name = Into::<&str>::into(core.kind); disp.section_line("Type", name); @@ -205,10 +188,8 @@ impl CpuDisplay { disp.section_line("MicroArch", &ma_str); } - if Self::should_show_core_codename(core, cpu_info, flags.verbose) - && let Some(codename) = &core.name - { - disp.section_line("Codename", codename); + if Self::should_show_core_codename(core, cpu_info, flags.verbose) { + disp.section_line_opt("Codename", core.name.as_deref()); } disp.section_line("Count", &core.count.to_string()); @@ -229,23 +210,17 @@ impl CpuDisplay { } } } else if let Some(core) = cpu_info.cores.first() { - println!("{}", disp.label("Cores")); + disp.print_label("Cores"); - if let Some(ref vendor_str) = core.implementer - && vendor_str != UNK - { - disp.section_line("Implementer", vendor_str); - } + disp.section_line_opt("Implementer", core.implementer.as_deref()); let ma_str: String = core.micro_arch.into(); if Self::should_show_core_micro_arch(core.micro_arch, flags.verbose) { disp.section_line("MicroArch", &ma_str); } - if Self::should_show_core_codename(core, cpu_info, flags.verbose) - && let Some(codename) = &core.name - { - disp.section_line("Codename", codename); + if Self::should_show_core_codename(core, cpu_info, flags.verbose) { + disp.section_line_opt("Codename", core.name.as_deref()); } disp.section_line("Count", &core.count.to_string()); @@ -343,21 +318,21 @@ mod tests { #[test] fn test_is_duplicate() { - assert!(is_duplicate("ARM Cortex-A53", "Cortex-A53")); - assert!(is_duplicate("Cortex-A53", "ARM Cortex-A53")); - assert!(is_duplicate("Apple Swift", "Swift")); - assert!(is_duplicate("AmpereOne", "AmpereOne")); - assert!(is_duplicate("cortex-a53", "CORTEX-A53")); - - assert!(!is_duplicate("ARM Cortex-A72", "Maya")); - assert!(!is_duplicate("Maya", "Cortex-A72")); - assert!(!is_duplicate("Apple A18 Pro", "Tahiti")); - assert!(!is_duplicate("Everest", "Tahiti")); - assert!(!is_duplicate("Sawtooth", "Tahiti")); - assert!(!is_duplicate("Apple M1", "Tonga")); - assert!(!is_duplicate("FireStorm", "Tonga")); - assert!(!is_duplicate("", "Maya")); - assert!(!is_duplicate("Maya", "")); + assert!(CpuDisplay::is_duplicate("ARM Cortex-A53", "Cortex-A53")); + assert!(CpuDisplay::is_duplicate("Cortex-A53", "ARM Cortex-A53")); + assert!(CpuDisplay::is_duplicate("Apple Swift", "Swift")); + assert!(CpuDisplay::is_duplicate("AmpereOne", "AmpereOne")); + assert!(CpuDisplay::is_duplicate("cortex-a53", "CORTEX-A53")); + + assert!(!CpuDisplay::is_duplicate("ARM Cortex-A72", "Maya")); + assert!(!CpuDisplay::is_duplicate("Maya", "Cortex-A72")); + assert!(!CpuDisplay::is_duplicate("Apple A18 Pro", "Tahiti")); + assert!(!CpuDisplay::is_duplicate("Everest", "Tahiti")); + assert!(!CpuDisplay::is_duplicate("Sawtooth", "Tahiti")); + assert!(!CpuDisplay::is_duplicate("Apple M1", "Tonga")); + assert!(!CpuDisplay::is_duplicate("FireStorm", "Tonga")); + assert!(!CpuDisplay::is_duplicate("", "Maya")); + assert!(!CpuDisplay::is_duplicate("Maya", "")); } #[test] diff --git a/src/common/display.rs b/src/common/display.rs index edeaa592..8f8621e5 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -68,6 +68,102 @@ impl CpuDisplay { format!("{}{s}{ANSI_RESET}", Self::ansi(code)) } + /// Outputs just the label without a value followed by a newline. + pub fn print_label(&self, s: &str) { + println!("{}", self.label(s)); + } + + /// Outputs a core cluster heading (e.g. "Core #1", "Core #2"). + pub fn core_heading(&self, index: usize) { + self.print_label(&format!("Core #{}", index + 1)); + } + + /// Formats a boolean as "Yes" or "No". + pub fn yes_no(b: bool) -> &'static str { + if b { "Yes" } else { "No" } + } + + /// Returns `singular` if `count == 1`, else `plural_form`. + pub fn plural(count: u32, singular: &'static str, plural_form: &'static str) -> &'static str { + if count == 1 { singular } else { plural_form } + } + + /// Case-insensitively checks if either string is contained in the other or if they are equal. + pub fn is_duplicate(a: &str, b: &str) -> bool { + if a.is_empty() || b.is_empty() { + return false; + } + let a_lower = a.to_ascii_lowercase(); + let b_lower = b.to_ascii_lowercase(); + a_lower == b_lower || a_lower.contains(&b_lower) || b_lower.contains(&a_lower) + } + + /// Formats physical core and logical thread counts (e.g. "4 cores (8 threads)" or "4 cores"). + pub fn format_core_threads(cores: u32, threads: u32) -> String { + if cores == 0 { + return String::new(); + } + if threads != cores && threads > 0 { + format!("{} cores ({} threads)", cores, threads) + } else { + format!("{} cores", cores) + } + } + + /// Outputs a simple line if the value is not UNK and not empty. + pub fn simple_line_if_known(&self, l: &str, v: &str) { + if v != UNK && !v.is_empty() { + self.simple_line(l, v); + } + } + + /// Outputs a section line if the value is not UNK and not empty. + pub fn section_line_if_known(&self, l: &str, v: &str) { + if v != UNK && !v.is_empty() { + self.section_line(l, v); + } + } + + /// Outputs a simple line if the optional value is present, not UNK, and not empty. + pub fn simple_line_opt>(&self, l: &str, v: Option) { + if let Some(val) = v { + self.simple_line_if_known(l, val.as_ref()); + } + } + + /// Outputs a section line if the optional value is present, not UNK, and not empty. + pub fn section_line_opt>(&self, l: &str, v: Option) { + if let Some(val) = v { + self.section_line_if_known(l, val.as_ref()); + } + } + + /// Outputs a simple line with a main value and parenthesized detail, e.g. "GenuineIntel (Intel)". + pub fn simple_line_with_detail(&self, l: &str, v: &str, detail: &str) { + self.simple_line(l, &format!("{v} ({detail})")); + } + + /// Outputs a section line with a main value and parenthesized detail, e.g. "GenuineIntel (Intel)". + pub fn section_line_with_detail(&self, l: &str, v: &str, detail: &str) { + self.section_line(l, &format!("{v} ({detail})")); + } + + /// Displays a formatted value, and additionally outputs the raw value if `verbose` is true and raw differs. + pub fn display_with_raw(&self, label: &str, formatted: &str, raw: Option<&str>, verbose: bool) { + if let Some(raw_val) = raw + && verbose + && raw_val != UNK + && !raw_val.is_empty() + && raw_val.trim() != formatted.trim() + { + self.section_line(label, formatted); + self.section_line(&format!("{label} (raw)"), raw_val); + self.newline(); + } else { + self.simple_line(label, formatted); + } + } + /// Outputs a formatted label and value with an additional newline if flags.compact is false pub fn simple_line(&self, l: &str, v: &str) { self.section_line(l, v); @@ -137,9 +233,8 @@ impl CpuDisplay { self.simple_line( "Topology", &alloc::format!( - "{} cores ({} threads) across {} core types", - total_cores, - total_threads, + "{} across {} core types", + Self::format_core_threads(total_cores, total_threads), cluster_count ), ); @@ -150,14 +245,10 @@ impl CpuDisplay { ); } } else if total_cores > 0 { - if total_threads != total_cores { - self.simple_line( - "Topology", - &alloc::format!("{} cores ({} threads)", total_cores, total_threads), - ); - } else { - self.simple_line("Topology", &alloc::format!("{} cores", total_cores)); - } + self.simple_line( + "Topology", + &Self::format_core_threads(total_cores, total_threads), + ); } } @@ -170,6 +261,13 @@ impl CpuDisplay { K: core::borrow::Borrow + Ord, { if !features.is_empty() { + if features.len() == 1 + && let Some(base_str) = features.get("Base") + { + self.simple_line("Features", base_str); + return; + } + let mut first = true; for key in keys { if let Some(feat_str) = features.get(*key) { @@ -311,15 +409,8 @@ impl CpuDisplay { #[cfg(not(dos_os))] pub fn display_system(&self, system: &str, flags: CliFlags) { - let formatted = &self.format_system_name(system); - - if flags.verbose && system != formatted { - self.section_line("System", formatted); - self.section_line("System (raw)", system); - self.newline(); - } else { - self.simple_line("System", formatted); - } + let formatted = self.format_system_name(system); + self.display_with_raw("System", &formatted, Some(system), flags.verbose); } /// Format the system name if it is a Mac, or other known string @@ -780,4 +871,59 @@ mod tests { "Power Mac G5 (Late 2005)" ); } + + #[test] + fn test_yes_no() { + assert_eq!(CpuDisplay::yes_no(true), "Yes"); + assert_eq!(CpuDisplay::yes_no(false), "No"); + } + + #[test] + fn test_plural() { + assert_eq!(CpuDisplay::plural(1, "core", "cores"), "core"); + assert_eq!(CpuDisplay::plural(0, "core", "cores"), "cores"); + assert_eq!(CpuDisplay::plural(4, "core", "cores"), "cores"); + } + + #[test] + fn test_is_duplicate() { + assert!(CpuDisplay::is_duplicate("ARM Cortex-A53", "Cortex-A53")); + assert!(CpuDisplay::is_duplicate("Cortex-A53", "ARM Cortex-A53")); + assert!(CpuDisplay::is_duplicate("Apple Swift", "Swift")); + assert!(CpuDisplay::is_duplicate("AmpereOne", "AmpereOne")); + assert!(CpuDisplay::is_duplicate("cortex-a53", "CORTEX-A53")); + + assert!(!CpuDisplay::is_duplicate("ARM Cortex-A72", "Maya")); + assert!(!CpuDisplay::is_duplicate("Maya", "Cortex-A72")); + assert!(!CpuDisplay::is_duplicate("", "Maya")); + assert!(!CpuDisplay::is_duplicate("Maya", "")); + } + + #[test] + fn test_format_core_threads() { + assert_eq!(CpuDisplay::format_core_threads(0, 0), ""); + assert_eq!(CpuDisplay::format_core_threads(4, 4), "4 cores"); + assert_eq!(CpuDisplay::format_core_threads(4, 8), "4 cores (8 threads)"); + assert_eq!(CpuDisplay::format_core_threads(1, 2), "1 cores (2 threads)"); + } + + #[test] + fn test_display_helpers_no_panic() { + let disp = CpuDisplay { + flags: CliFlags { + color: false, + compact: false, + verbose: false, + }, + }; + disp.print_label("Cores"); + disp.core_heading(0); + disp.simple_line_if_known("MicroArch", "Zen 4"); + disp.simple_line_if_known("MicroArch", UNK); + disp.simple_line_opt("Process", Some("4nm")); + disp.simple_line_opt("Process", None::<&str>); + disp.simple_line_with_detail("Vendor", "AuthenticAMD", "AMD"); + disp.display_with_raw("System", "MacBook Pro", Some("MacBookPro18,1"), false); + disp.display_with_raw("System", "MacBook Pro", Some("MacBookPro18,1"), true); + } } diff --git a/src/common/os/android.rs b/src/common/os/android.rs index d25ec4be..f73427cb 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -203,45 +203,6 @@ pub fn extract_soc(props: &HashMap) -> Option { // Helpers for CPU Lists & /proc/cpuinfo // ---------------------------------------------------------------------------- -/// Parse a Linux/Android CPU list string (e.g., "0-3", "0-3,8-11", "0") and return -/// the total number of CPUs it represents. -pub fn parse_cpu_list_count(s: &str) -> u32 { - let mut count = 0; - for part in s.trim().split(',') { - let part = part.trim(); - if let Some(dash) = part.find('-') { - if let (Ok(start), Ok(end)) = - (part[..dash].parse::(), part[dash + 1..].parse::()) - { - count += end.saturating_sub(start) + 1; - } - } else if part.parse::().is_ok() { - count += 1; - } - } - count -} - -/// Expand a Linux/Android CPU list string into a vector of individual CPU IDs. -pub fn expand_cpu_list(s: &str) -> Vec { - let mut cpus = Vec::new(); - for part in s.trim().split(',') { - let part = part.trim(); - if let Some(dash) = part.find('-') { - if let (Ok(start), Ok(end)) = - (part[..dash].parse::(), part[dash + 1..].parse::()) - { - for cpu in start..=end { - cpus.push(cpu); - } - } - } else if let Ok(cpu) = part.parse::() { - cpus.push(cpu); - } - } - cpus -} - fn get_soc_cpuinfo() -> Option { let cpuinfo = get_proc_cpuinfo_data(); if let Some(last) = cpuinfo.last() @@ -253,39 +214,6 @@ fn get_soc_cpuinfo() -> Option { None } -fn get_devicetree_compatible() -> Option>> { - if let Ok(raw) = std::fs::read_to_string("/proc/device-tree/compatible") { - let res: Vec<_> = raw - .split('\0') - .filter(|s| !s.is_empty()) - .map(|p| -> Vec<_> { p.split(',').map(String::from).collect() }) - .collect(); - - return Some(res); - } - None -} - -fn format_compatible_pair(pair: Vec) -> String { - if pair.len() < 2 { - return pair[0].clone(); - } - let raw_vendor = pair[0].clone(); - let raw_model = pair[1].clone(); - let vendor = cleanup_soc_vendor(raw_vendor.as_str()); - let model = if raw_model - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') - && raw_model.chars().any(|c| c.is_ascii_lowercase()) - && raw_model.chars().any(|c| c.is_ascii_digit()) - { - raw_model.to_uppercase() - } else { - raw_model - }; - format!("{vendor} {model}") -} - fn get_soc_devicetree() -> Option { if let Some(raw_pairs) = get_devicetree_compatible() && let Some(pair) = raw_pairs.last().cloned() @@ -295,27 +223,6 @@ fn get_soc_devicetree() -> Option { None } -pub fn get_proc_cpuinfo_data() -> Vec> { - let content = match std::fs::read_to_string("/proc/cpuinfo") { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - - content - .split("\n\n") - .filter(|s| !s.trim().is_empty()) - .map(|section| { - let mut map = HashMap::new(); - for line in section.lines() { - if let Some((key, val)) = line.split_once(':') { - map.insert(key.trim().to_string(), val.trim().to_string()); - } - } - map - }) - .collect() -} - // ---------------------------------------------------------------------------- // TOSData Implementation // ---------------------------------------------------------------------------- diff --git a/src/common/os/common.rs b/src/common/os/common.rs index c9f37215..297d26a4 100644 --- a/src/common/os/common.rs +++ b/src/common/os/common.rs @@ -67,3 +67,140 @@ pub fn is_known_hypervisor_vendor(vendor: &str) -> bool { ]; HYPERVISORS.contains(&vendor.as_str()) } + +/// Parse a Linux/Android CPU list string (e.g., "0-3", "0-3,8-11", "0") and return +/// the total number of CPUs it represents. +pub fn parse_cpu_list_count(s: &str) -> u32 { + let mut count = 0; + for part in s.trim().split(',') { + let part = part.trim(); + if let Some(dash) = part.find('-') { + if let (Ok(start), Ok(end)) = + (part[..dash].parse::(), part[dash + 1..].parse::()) + { + count += end.saturating_sub(start) + 1; + } + } else if part.parse::().is_ok() { + count += 1; + } + } + count +} + +/// Expand a Linux/Android CPU list string into a vector of individual CPU IDs. +pub fn expand_cpu_list(s: &str) -> alloc::vec::Vec { + let mut cpus = alloc::vec::Vec::new(); + for part in s.trim().split(',') { + let part = part.trim(); + if let Some(dash) = part.find('-') { + if let (Ok(start), Ok(end)) = + (part[..dash].parse::(), part[dash + 1..].parse::()) + { + for cpu in start..=end { + cpus.push(cpu); + } + } + } else if let Ok(cpu) = part.parse::() { + cpus.push(cpu); + } + } + cpus +} + +#[cfg(std_os)] +pub fn format_compatible_pair(pair: alloc::vec::Vec) -> String { + if pair.len() < 2 { + return pair[0].clone(); + } + + let raw_vendor = pair[0].clone(); + let raw_model = pair[1].clone(); + + let vendor = crate::common::cleanup_soc_vendor(raw_vendor.as_str()); + + let model = if raw_model + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') + && raw_model.chars().any(|c| c.is_ascii_lowercase()) + && raw_model.chars().any(|c| c.is_ascii_digit()) + { + raw_model.to_uppercase() + } else { + raw_model + }; + + alloc::format!("{vendor} {model}") +} + +#[cfg(std_os)] +pub fn get_devicetree_compatible() -> Option>> { + if let Ok(raw) = std::fs::read_to_string("/proc/device-tree/compatible") { + let res: alloc::vec::Vec<_> = raw + .split('\0') + .filter(|s| !s.is_empty()) + .map(|p| -> alloc::vec::Vec<_> { + // Since Mac Model strings contain commas, we don't want to split on those + if !(p.contains("Power") || p.contains("Mac")) { + p.split(',').map(String::from).collect() + } else { + alloc::vec![String::from(p)] + } + }) + .collect(); + + return Some(res); + } + + None +} + +#[cfg(std_os)] +pub fn get_proc_cpuinfo_data() -> std::vec::Vec> { + let content = match std::fs::read_to_string("/proc/cpuinfo") { + Ok(c) => c, + Err(_) => return std::vec::Vec::new(), + }; + + content + .split("\n\n") + .filter(|s| !s.trim().is_empty()) + .map(|section| { + let mut map = std::collections::HashMap::new(); + for line in section.lines() { + if let Some((key, val)) = line.split_once(':') { + map.insert(key.trim().to_string(), val.trim().to_string()); + } + } + map + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_cpu_list_count() { + assert_eq!(parse_cpu_list_count("0-7"), 8); + assert_eq!(parse_cpu_list_count("0-3,4-7"), 8); + assert_eq!(parse_cpu_list_count("0"), 1); + assert_eq!(parse_cpu_list_count(""), 0); + } + + #[test] + fn test_expand_cpu_list() { + assert_eq!(expand_cpu_list("0-3"), alloc::vec![0, 1, 2, 3]); + assert_eq!(expand_cpu_list("0,4,7"), alloc::vec![0, 4, 7]); + } + + #[test] + #[cfg(std_os)] + fn test_format_compatible_pair() { + let pair = alloc::vec!["qcom".to_string(), "sm8450".to_string()]; + assert_eq!(format_compatible_pair(pair), "Qualcomm SM8450"); + + let single = alloc::vec!["Apple".to_string()]; + assert_eq!(format_compatible_pair(single), "Apple"); + } +} diff --git a/src/common/os/linux.rs b/src/common/os/linux.rs index 2bfea87c..16d143cf 100644 --- a/src/common/os/linux.rs +++ b/src/common/os/linux.rs @@ -1,9 +1,10 @@ #![cfg(target_os = "linux")] use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, cleanup_soc_vendor, + DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, expand_cpu_list, + format_compatible_pair, get_devicetree_compatible, get_proc_cpuinfo_data, parse_cpu_list_count, }; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fs; use std::path::Path; @@ -13,45 +14,6 @@ use crate::common::{Cache, CacheLevel, CacheType, Level1Cache}; #[cfg(any(arm_cpu, test))] use std::collections::BTreeMap; -/// Parse a Linux CPU list string (e.g., "0-3", "0-3,8-11", "0") and return -/// the total number of CPUs it represents. -fn parse_cpu_list_count(s: &str) -> u32 { - let mut count = 0; - for part in s.trim().split(',') { - let part = part.trim(); - if let Some(dash) = part.find('-') { - if let (Ok(start), Ok(end)) = - (part[..dash].parse::(), part[dash + 1..].parse::()) - { - count += end.saturating_sub(start) + 1; - } - } else if part.parse::().is_ok() { - count += 1; - } - } - count -} - -/// Expand a Linux CPU list string into a vector of individual CPU IDs. -pub fn expand_cpu_list(s: &str) -> Vec { - let mut cpus = Vec::new(); - for part in s.trim().split(',') { - let part = part.trim(); - if let Some(dash) = part.find('-') { - if let (Ok(start), Ok(end)) = - (part[..dash].parse::(), part[dash + 1..].parse::()) - { - for cpu in start..=end { - cpus.push(cpu); - } - } - } else if let Ok(cpu) = part.parse::() { - cpus.push(cpu); - } - } - cpus -} - fn get_soc_cpuinfo() -> Option { let cpuinfo = get_proc_cpuinfo_data(); if let Some(last) = cpuinfo.last() @@ -63,51 +25,6 @@ fn get_soc_cpuinfo() -> Option { None } -pub fn get_devicetree_compatible() -> Option>> { - if let Ok(raw) = std::fs::read_to_string("/proc/device-tree/compatible") { - let res: Vec<_> = raw - .split('\0') - .filter(|s| !s.is_empty()) - .map(|p| -> Vec<_> { - // Since Mac Model strings contain commas, we don't want to split on those - if !(p.contains("Power") || p.contains("Mac")) { - p.split(",").map(String::from).collect() - } else { - vec![String::from(p)] - } - }) - .collect(); - - return Some(res); - } - - None -} - -pub fn format_compatible_pair(pair: Vec) -> String { - if pair.len() < 2 { - return pair[0].clone(); - } - - let raw_vendor = pair[0].clone(); - let raw_model = pair[1].clone(); - - let vendor = cleanup_soc_vendor(raw_vendor.as_str()); - - let model = if raw_model - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_') - && raw_model.chars().any(|c| c.is_ascii_lowercase()) - && raw_model.chars().any(|c| c.is_ascii_digit()) - { - raw_model.to_uppercase() - } else { - raw_model - }; - - format!("{vendor} {model}") -} - use super::{is_generic_value, is_known_hypervisor_vendor}; /// Read a DMI field from sysfs, trying both the virtual and class mount @@ -222,27 +139,6 @@ fn get_soc_devicetree() -> Option { None } -pub fn get_proc_cpuinfo_data() -> Vec> { - let content = match std::fs::read_to_string("/proc/cpuinfo") { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - - content - .split("\n\n") - .filter(|s| !s.trim().is_empty()) - .map(|section| { - let mut map = HashMap::new(); - for line in section.lines() { - if let Some((key, val)) = line.split_once(':') { - map.insert(key.trim().to_string(), val.trim().to_string()); - } - } - map - }) - .collect() -} - impl TOSData for OS { fn get_soc() -> Option { if let Some(soc) = get_soc_cpuinfo() { diff --git a/src/common/os/mod.rs b/src/common/os/mod.rs index 523a6dcb..15cabafb 100644 --- a/src/common/os/mod.rs +++ b/src/common/os/mod.rs @@ -35,9 +35,6 @@ pub use common::*; #[cfg(target_os = "android")] pub use android::*; -#[cfg(target_os = "linux")] -pub use linux::*; - #[cfg(target_os = "macos")] pub use macos::*; diff --git a/src/ppc/display.rs b/src/ppc/display.rs index 27a04e88..c4396c32 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -19,9 +19,7 @@ impl TCpuDisplay for Cpu { disp.simple_line("Model", self.cpu_arch.marketing_name); disp.simple_line("MicroArch", self.cpu_arch.micro_arch.into()); disp.simple_line("Codename", self.cpu_arch.code_name); - if let Some(tech) = self.cpu_arch.technology { - disp.simple_line("Process", tech); - } + disp.simple_line_opt("Process", self.cpu_arch.technology); let total_cores = self.total_cores(); let total_threads = self.total_threads(); diff --git a/src/riscv/display.rs b/src/riscv/display.rs index 2494301c..f3e49e3f 100644 --- a/src/riscv/display.rs +++ b/src/riscv/display.rs @@ -19,10 +19,10 @@ impl CpuDisplay { let ma = cpu_info.cpu_arch.micro_arch.as_str(); if ma != UNK { disp.simple_line("CPU Core", ma); - } else if let Some(uarch) = cpu_info.raw.get("uarch") { - if !uarch.is_empty() { - disp.simple_line("CPU Core", &format_uarch(uarch)); - } + } else if let Some(uarch) = cpu_info.raw.get("uarch") + && !uarch.is_empty() + { + disp.simple_line("CPU Core", &format_uarch(uarch)); } else { let cpu_vendor_str: &str = cpu_info.cpu_arch.vendor.into(); disp.simple_line("CPU Vendor", cpu_vendor_str); @@ -32,9 +32,7 @@ impl CpuDisplay { disp.simple_line("Codename", cpu_info.cpu_arch.code_name); } - if let Some(tech) = cpu_info.cpu_arch.technology { - disp.simple_line("Process Node", tech); - } + disp.simple_line_opt("Process Node", cpu_info.cpu_arch.technology); // Display topology & per-core details if cpu_info.is_hybrid() { @@ -46,15 +44,12 @@ impl CpuDisplay { ); for (i, core) in cpu_info.cores.iter().enumerate() { - let core_label = format!("Core #{}", i + 1); - println!("{}", disp.label(&core_label)); + disp.core_heading(i); let type_str: &str = core.kind.into(); disp.section_line("Type", type_str); - if let Some(name) = &core.name { - disp.section_line("MicroArch", name); - } + disp.section_line_opt("MicroArch", core.name.as_deref()); disp.section_line("Count", &core.count.to_string()); diff --git a/src/x86/display.rs b/src/x86/display.rs index 03726148..780bae13 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -10,10 +10,6 @@ use alloc::format; #[cfg(not(dos_real))] use alloc::string::String; -fn yes_no(b: bool) -> &'static str { - if b { "Yes" } else { "No" } -} - #[cfg(not(dos_real))] impl CpuDisplay { /// Computes the number of cache instances on x86 taking SMT / APIC ID allocation into account. @@ -58,8 +54,8 @@ impl Cpu { let cpuid = self.has_cpuid; if flags.verbose { - disp.simple_line("CPUID", yes_no(cpuid)); - disp.simple_line("Overdrive", yes_no(overdrive)); + disp.simple_line("CPUID", CpuDisplay::yes_no(cpuid)); + disp.simple_line("Overdrive", CpuDisplay::yes_no(overdrive)); } else { if !cpuid { disp.simple_line("CPUID", "No"); @@ -76,50 +72,38 @@ impl Cpu { if disp_model != UNK { if raw_model.eq(UNK) { disp.simple_line("Model (synth)", &disp_model); - } else if raw_model.trim().eq(&disp_model) { - disp.simple_line("Model", &disp_model); } else { - println!("{}{}", disp.label("Model"), disp_model); - - if flags.verbose { - println!("{}{}", disp.label("Model (raw)"), raw_model); - } - - disp.newline(); + disp.display_with_raw("Model", &disp_model, Some(&raw_model), flags.verbose); } } } fn print_topology(&self, flags: CliFlags, disp: &CpuDisplay) { if self.is_hybrid() { - println!( - "{}{} cores ({} threads) across {} core types", - disp.label("Topology"), - self.topology.cores.count, - self.topology.threads.count, - self.cores.len() + disp.simple_line( + "Topology", + &format!( + "{} across {} core types", + CpuDisplay::format_core_threads( + self.topology.cores.count, + self.topology.threads.count + ), + self.cores.len() + ), ); - disp.newline(); for (i, core) in self.cores.iter().enumerate() { - let core_label = format!("Core #{}", i + 1); - println!("{}", disp.label(&core_label)); + disp.core_heading(i); let type_str: &str = core.kind.into(); disp.section_line("Type", type_str); - if let Some(name) = &core.name { - disp.section_line("Codename", name); - } + disp.section_line_opt("Codename", core.name.as_deref()); - if core.count != core.threads { - disp.section_line( - "Topology", - &format!("{} cores ({} threads)", core.count, core.threads), - ); - } else { - disp.section_line("Topology", &format!("{} cores", core.count)); - } + disp.section_line( + "Topology", + &CpuDisplay::format_core_threads(core.count, core.threads), + ); disp.display_frequency( core.speed, @@ -145,40 +129,29 @@ impl Cpu { let multi_core = self.topology.cores.count > 1 || self.topology.sockets.count > 1; if multi_core || flags.verbose { - let lbl = disp.label("Topology"); - let socket_str = if self.topology.sockets.count == 1 { - "socket" - } else { - "sockets" - }; - let core_str = if self.topology.cores.count == 1 { - "core" - } else { - "cores" - }; - let thread_str = if self.topology.threads.count == 1 { - "thread" - } else { - "threads" - }; + let socket_str = CpuDisplay::plural(self.topology.sockets.count, "socket", "sockets"); + let core_str = CpuDisplay::plural(self.topology.cores.count, "core", "cores"); + let thread_str = CpuDisplay::plural(self.topology.threads.count, "thread", "threads"); if self.topology.sockets.count > 1 || flags.verbose { - println!( - "{lbl}{} {socket_str}, {} {core_str}, {} {thread_str}", - self.topology.sockets.count, - self.topology.cores.count, - self.topology.threads.count, - ); - } else if self.topology.cores.count != self.topology.threads.count { - println!( - "{lbl}{} cores ({} threads)", - self.topology.cores.count, self.topology.threads.count + disp.simple_line( + "Topology", + &format!( + "{} {socket_str}, {} {core_str}, {} {thread_str}", + self.topology.sockets.count, + self.topology.cores.count, + self.topology.threads.count, + ), ); } else { - println!("{lbl}{} cores", self.topology.cores.count); + disp.simple_line( + "Topology", + &CpuDisplay::format_core_threads( + self.topology.cores.count, + self.topology.threads.count, + ), + ); } - - disp.newline(); } } @@ -243,43 +216,6 @@ impl Cpu { disp.newline(); } } -} - -// Cpu features display -impl Cpu { - fn print_simple_features_list(&self, disp: &CpuDisplay) { - disp.simple_line( - "Features", - self.features - .get("Base") - .expect("There should be at least one key in the features BTreeMap."), - ); - } - - fn print_full_features_list(&self, disp: &CpuDisplay) { - let keys = [ - "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", - ]; - for key in keys { - if self.features.contains_key(key) { - if key == "Base" { - println!( - "{}{}", - disp.inline_sublabel("Features", "Base"), - self.features.get(key).expect("Missing Base key?") - ) - } else { - println!( - "{}{}", - disp.sublabel(key), - self.features - .get(key) - .expect("Somehow the key in the features BTreeMap disappeared!") - ); - } - } - } - } #[cfg(not(dos_real))] fn print_centaur_features(&self, flags: CliFlags, disp: &CpuDisplay) { @@ -309,20 +245,16 @@ impl Cpu { #[allow(unused_variables)] fn print_features(&self, flags: CliFlags, disp: &CpuDisplay) { if !self.features.is_empty() { - // Simple features list - if self.features.len() == 1 { - self.print_simple_features_list(disp); - } else { - self.print_full_features_list(disp); - } + let keys = [ + "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", + ]; + disp.display_features(&self.features, &keys); // Centaur features list #[cfg(not(dos_real))] if is_centaur() { self.print_centaur_features(flags, disp); } - - disp.newline(); } } } @@ -422,31 +354,20 @@ impl TCpuDisplay for Cpu { // Vendor_string (brand_name) if self.arch.brand_name != UNK { - println!( - "{}{} ({})", - disp.label("Vendor"), - self.arch.vendor_string, - self.arch.brand_name - ); - - disp.newline(); + disp.simple_line_with_detail("Vendor", &self.arch.vendor_string, self.arch.brand_name); } // Hypervisor vendor_string (brand_name) #[cfg(not(dos_real))] if let Some(hyp_str) = &self.hyp_vendor_str { let hyp = HypervisorBrand::from(hyp_str.as_str()); - println!("{}{} ({})", disp.label("Hypervisor"), hyp_str, hyp.to_str()); - - disp.newline(); + disp.simple_line_with_detail("Hypervisor", hyp_str, hyp.to_str()); } // Cpu model string self.print_model(flags, &disp); - if ma != UNK { - disp.simple_line("MicroArch", ma); - } + disp.simple_line_if_known("MicroArch", ma); if !(self.arch.code_name == "Unknown" || self.arch.code_name == ma @@ -456,14 +377,10 @@ impl TCpuDisplay for Cpu { } // Process node - if let Some(tech) = &self.arch.technology { - disp.simple_line("Process Node", tech); - } + disp.simple_line_opt("Process Node", self.arch.technology); // Easter Egg (AMD K6, K8, Jaguar or Rise mp6) - if let Some(easter_egg) = &self.easter_egg { - disp.simple_line("Easter Egg", easter_egg); - } + disp.simple_line_opt("Easter Egg", self.easter_egg.as_deref()); // Overdrive, CPUID support, etc self.print_misc_flags(flags, &disp); From 2b95a98ec4cd2bb8ef3cb52d5ee6cced4ad421b6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 13:25:38 -0400 Subject: [PATCH 18/30] Update display logic to show both cores and threads if there are multiple cores or multiple threads. Will also now show threads for single-core cpus. --- src/common/display.rs | 41 +++++++++++++++++------------------------ src/x86/display.rs | 4 +++- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/common/display.rs b/src/common/display.rs index 8f8621e5..b97638a1 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -98,16 +98,14 @@ impl CpuDisplay { a_lower == b_lower || a_lower.contains(&b_lower) || b_lower.contains(&a_lower) } - /// Formats physical core and logical thread counts (e.g. "4 cores (8 threads)" or "4 cores"). + /// Formats physical core and logical thread counts with proper pluralization (e.g. "4 cores (8 threads)", "2 cores (2 threads)", "1 core (2 threads)"). pub fn format_core_threads(cores: u32, threads: u32) -> String { - if cores == 0 { + if cores == 0 && threads == 0 { return String::new(); } - if threads != cores && threads > 0 { - format!("{} cores ({} threads)", cores, threads) - } else { - format!("{} cores", cores) - } + let core_str = Self::plural(cores, "core", "cores"); + let thread_str = Self::plural(threads, "thread", "threads"); + format!("{cores} {core_str} ({threads} {thread_str})") } /// Outputs a simple line if the value is not UNK and not empty. @@ -229,21 +227,14 @@ impl CpuDisplay { cluster_count: usize, ) { if is_hybrid { - if total_threads != total_cores { - self.simple_line( - "Topology", - &alloc::format!( - "{} across {} core types", - Self::format_core_threads(total_cores, total_threads), - cluster_count - ), - ); - } else { - self.simple_line( - "Topology", - &alloc::format!("{} cores across {} core types", total_cores, cluster_count), - ); - } + self.simple_line( + "Topology", + &alloc::format!( + "{} across {} core types", + Self::format_core_threads(total_cores, total_threads), + cluster_count + ), + ); } else if total_cores > 0 { self.simple_line( "Topology", @@ -902,9 +893,11 @@ mod tests { #[test] fn test_format_core_threads() { assert_eq!(CpuDisplay::format_core_threads(0, 0), ""); - assert_eq!(CpuDisplay::format_core_threads(4, 4), "4 cores"); + assert_eq!(CpuDisplay::format_core_threads(4, 4), "4 cores (4 threads)"); assert_eq!(CpuDisplay::format_core_threads(4, 8), "4 cores (8 threads)"); - assert_eq!(CpuDisplay::format_core_threads(1, 2), "1 cores (2 threads)"); + assert_eq!(CpuDisplay::format_core_threads(1, 2), "1 core (2 threads)"); + assert_eq!(CpuDisplay::format_core_threads(1, 1), "1 core (1 thread)"); + assert_eq!(CpuDisplay::format_core_threads(2, 2), "2 cores (2 threads)"); } #[test] diff --git a/src/x86/display.rs b/src/x86/display.rs index 780bae13..923bf285 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -126,7 +126,9 @@ impl Cpu { return; } - let multi_core = self.topology.cores.count > 1 || self.topology.sockets.count > 1; + let multi_core = self.topology.cores.count > 1 + || self.topology.threads.count > 1 + || self.topology.sockets.count > 1; if multi_core || flags.verbose { let socket_str = CpuDisplay::plural(self.topology.sockets.count, "socket", "sockets"); From dac38e045510646f0dd0cd2bd201fca0e1e1bf9e Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 13:34:06 -0400 Subject: [PATCH 19/30] Update makefile and justfile to install missing compile targets for check-all recipe --- Makefile | 6 ++++-- justfile | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 14a448b4..35119753 100644 --- a/Makefile +++ b/Makefile @@ -66,10 +66,12 @@ check-dos: check-dos32a check-dos-real # Compile check for Risc V check-riscv: + @if ! rustup target list --installed | grep -q riscv64gc-unknown-linux-gnu; then rustup target add riscv64gc-unknown-linux-gnu; fi cargo check --target riscv64gc-unknown-linux-gnu # Compile check for Windows ARM check-win-arm: + @if ! rustup target list --installed | grep -q aarch64-pc-windows-msvc; then rustup target add aarch64-pc-windows-msvc; fi cargo check --target aarch64-pc-windows-msvc # Compile check for 32-bit Linux 486 @@ -109,7 +111,7 @@ build-release: _build-dos-tools: # Fetch required tools (if they aren't already installed) - @if ! rustup component list --installed --toolchain nightly-x86_64-unknown-linux-gnu | grep -q rust-src; then rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu; fi + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi # Build for DOS (EXE format) build-dos-real: _build-dos-tools @@ -119,7 +121,7 @@ build-dos-real: _build-dos-tools _build-dos32a-tools: # Fetch required tools (if they aren't already installed) - @if ! rustup component list --installed --toolchain nightly-x86_64-unknown-linux-gnu | grep -q rust-src; then rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu; fi + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi _build-dos32a-rustid: _build-dos32a-tools @RUSTFLAGS="-C link-arg=-Tbuild-config/link-dos32a.x -C link-arg=--emit-relocs -C strip=none" cargo +nightly build -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos32a.json --features="dos32a-build" --bin dos_rustid --release diff --git a/justfile b/justfile index 05aeb75a..79326372 100644 --- a/justfile +++ b/justfile @@ -45,10 +45,12 @@ check-dos: check-dos32a check-dos-real # Compile check for Risc V check-riscv: + @if ! rustup target list --installed | grep -q riscv64gc-unknown-linux-gnu; then rustup target add riscv64gc-unknown-linux-gnu; fi cargo check --target riscv64gc-unknown-linux-gnu # Compile check for Windows ARM check-win-arm: + @if ! rustup target list --installed | grep -q aarch64-pc-windows-msvc; then rustup target add aarch64-pc-windows-msvc; fi cargo check --target aarch64-pc-windows-msvc # Compile check for 32-bit Linux 486 @@ -88,7 +90,7 @@ build-release: _build-dos-tools: # Fetch required tools (if they aren't already installed) - @if ! rustup component list --installed --toolchain nightly-x86_64-unknown-linux-gnu | grep -q rust-src; then rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu; fi + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi # Build for DOS (EXE format) build-dos-real: _build-dos-tools @@ -98,7 +100,7 @@ build-dos-real: _build-dos-tools _build-dos32a-tools: # Fetch required tools (if they aren't already installed) - @if ! rustup component list --installed --toolchain nightly-x86_64-unknown-linux-gnu | grep -q rust-src; then rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu; fi + @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi _build-dos32a-rustid: _build-dos32a-tools @RUSTFLAGS="-C link-arg=-Tbuild-config/link-dos32a.x -C link-arg=--emit-relocs -C strip=none" cargo +nightly build -Zjson-target-spec -Z build-std=core,alloc,panic_abort --target build-config/i486-dos32a.json --features="dos32a-build" --bin dos_rustid --release From fdb17628bd9459990cd5acf3e929faf71d9385ad Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 14:19:44 -0400 Subject: [PATCH 20/30] De-duplicate a bunch of os detection logic --- src/arm/display.rs | 6 +- src/arm/os/android.rs | 344 ----------------------------------- src/arm/os/bsd.rs | 9 +- src/arm/os/linux.rs | 24 +-- src/arm/os/mod.rs | 15 +- src/arm/os/windows.rs | 12 +- src/common/display.rs | 5 + src/common/os/android.rs | 219 +--------------------- src/common/os/common.rs | 103 +++++++++++ src/common/os/linux.rs | 213 ++-------------------- src/common/os/linux_sysfs.rs | 215 ++++++++++++++++++++++ src/common/os/mod.rs | 6 + src/ppc/cpu.rs | 30 +-- src/ppc/display.rs | 3 +- src/riscv/display.rs | 7 +- 15 files changed, 375 insertions(+), 836 deletions(-) delete mode 100644 src/arm/os/android.rs create mode 100644 src/common/os/linux_sysfs.rs diff --git a/src/arm/display.rs b/src/arm/display.rs index f5f210c6..83d26a3d 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -202,8 +202,7 @@ impl CpuDisplay { }, ); - let cc = |s| CpuDisplay::cache_count(s, core.count); - disp.display_cache(core.cache, &cc, 0); + disp.display_core_cache(core.cache, core.count, 0); if core.cache.is_none() { disp.newline(); @@ -227,8 +226,7 @@ impl CpuDisplay { disp.display_frequency(core.speed, flags); - let cc = |s| CpuDisplay::cache_count(s, core.count); - disp.display_cache(core.cache, &cc, 0); + disp.display_core_cache(core.cache, core.count, 0); } // Display features diff --git a/src/arm/os/android.rs b/src/arm/os/android.rs deleted file mode 100644 index 2db521c8..00000000 --- a/src/arm/os/android.rs +++ /dev/null @@ -1,344 +0,0 @@ -#![cfg(target_os = "android")] - -//! Android-specific ARM CPU feature detection. -//! -//! Uses core affinity pinning + MRS (`MIDR_EL1` via `HWCAP_CPUID` kernel trap) -//! and text-based parsing of `/proc/cpuinfo` "Features" line. - -use super::OsCpuInfo; -use crate::arm::brand::Vendor; -use crate::arm::micro_arch::*; -use crate::common::DataSource; -use crate::common::get_proc_cpuinfo_data; -use std::collections::{BTreeMap, HashSet}; - -/// Android-specific CPU detection via core affinity MRS, sysfs, and /proc/cpuinfo fallback. -pub fn detect() -> OsCpuInfo { - let mut midrs: HashSet = HashSet::new(); - let mut all_midrs: Vec = Vec::new(); - let mut midr_source = DataSource::CpuLookupTable; - - #[cfg(not(target_arch = "arm"))] - if let Some(core_ids) = core_affinity::get_core_ids() { - for core_id in core_ids { - core_affinity::set_for_current(core_id); - let midr_val = crate::arm::get_midr(); - let midr = Midr::new(midr_val); - midrs.insert(midr); - all_midrs.push(midr); - } - } else { - let midr_val = crate::arm::get_midr(); - let midr = Midr::new(midr_val); - midrs.insert(midr); - all_midrs.push(midr); - } - - // On 32-bit ARM or when MRS returned uniform value on big.LITTLE, - // check sysfs or /proc/cpuinfo - #[cfg(target_arch = "arm")] - { - let fallback_midrs = detect_android_midrs(); - if !fallback_midrs.is_empty() { - for m_val in fallback_midrs { - let midr = Midr::new(m_val); - midrs.insert(midr); - all_midrs.push(midr); - } - midr_source = DataSource::LinuxProcCpuinfo; - } - } - - #[cfg(not(target_arch = "arm"))] - { - let fallback_midrs = detect_android_midrs(); - if !fallback_midrs.is_empty() - && (fallback_midrs.len() > all_midrs.len() || midrs.len() <= 1) - { - all_midrs.clear(); - midrs.clear(); - for m_val in fallback_midrs { - let midr = Midr::new(m_val); - midrs.insert(midr); - all_midrs.push(midr); - } - midr_source = DataSource::LinuxProcCpuinfo; - } - } - - let primary_midr = midrs.iter().next().copied().unwrap_or(Midr::default()); - let vendor: String = Vendor::from(primary_midr.implementer).into(); - let cpu_arch = CpuArch::find( - primary_midr.implementer, - primary_midr.part, - primary_midr.variant, - ); - let cores = super::detect_cores(&all_midrs); - - OsCpuInfo { - midrs, - vendor, - cpu_arch, - cores, - model: String::new(), - raw: BTreeMap::new(), - midr_source, - features_source: DataSource::LinuxProcCpuinfo, - } -} - -/// Reads MIDR values from sysfs or /proc/cpuinfo across all CPU cores. -/// Handles big.LITTLE / DynamIQ heterogeneous topologies and offline cores. -fn detect_android_midrs() -> Vec { - // 1. Determine all expected CPUs from sysfs /possible or /present - let mut possible_cpus = Vec::new(); - if let Ok(content) = std::fs::read_to_string("/sys/devices/system/cpu/possible") { - possible_cpus = crate::common::expand_cpu_list(&content); - } - if possible_cpus.is_empty() - && let Ok(content) = std::fs::read_to_string("/sys/devices/system/cpu/present") - { - possible_cpus = crate::common::expand_cpu_list(&content); - } - if possible_cpus.is_empty() { - let mut missing_streak = 0; - for i in 0..256 { - let cpu_dir = format!("/sys/devices/system/cpu/cpu{}", i); - if std::path::Path::new(&cpu_dir).exists() { - possible_cpus.push(i); - missing_streak = 0; - } else { - missing_streak += 1; - if missing_streak >= 8 && i > 8 { - break; - } - } - } - } - - // 2. Read sysfs midr_el1 for each possible CPU - let mut sysfs_midrs: BTreeMap = BTreeMap::new(); - for &cpu_id in &possible_cpus { - let path = format!( - "/sys/devices/system/cpu/cpu{}/regs/identification/midr_el1", - cpu_id - ); - if let Ok(content) = std::fs::read_to_string(&path) - && let Ok(midr) = usize::from_str_radix(content.trim().trim_start_matches("0x"), 16) - { - sysfs_midrs.insert(cpu_id, midr); - } - } - - // 3. For any offline CPU missing sysfs midr_el1, infer from cluster siblings - for &cpu_id in &possible_cpus { - if !sysfs_midrs.contains_key(&cpu_id) { - let rel_path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/related_cpus", cpu_id); - let sibling_cpus = std::fs::read_to_string(&rel_path) - .ok() - .map(|s| crate::common::expand_cpu_list(&s)) - .or_else(|| { - let sib_path = format!( - "/sys/devices/system/cpu/cpu{}/topology/core_siblings_list", - cpu_id - ); - std::fs::read_to_string(&sib_path) - .ok() - .map(|s| crate::common::expand_cpu_list(&s)) - }); - - if let Some(siblings) = sibling_cpus { - for sib in siblings { - if let Some(&known_midr) = sysfs_midrs.get(&sib) { - sysfs_midrs.insert(cpu_id, known_midr); - break; - } - } - } - } - } - - // 4. Parse /proc/cpuinfo per-processor blocks - let mut cpuinfo_midrs: BTreeMap = BTreeMap::new(); - let mut cpuinfo_list: Vec = Vec::new(); - let cpuinfo = get_proc_cpuinfo_data(); - for (idx, map) in cpuinfo.iter().enumerate() { - let impl_ = map.get("CPU implementer").and_then(|s| { - usize::from_str_radix( - s.split_whitespace() - .next() - .unwrap_or("") - .trim_start_matches("0x"), - 16, - ) - .ok() - }); - let part = map.get("CPU part").and_then(|s| { - usize::from_str_radix( - s.split_whitespace() - .next() - .unwrap_or("") - .trim_start_matches("0x"), - 16, - ) - .ok() - }); - if let (Some(i), Some(p)) = (impl_, part) { - let var = map.get("CPU variant").and_then(|s| { - usize::from_str_radix( - s.split_whitespace() - .next() - .unwrap_or("") - .trim_start_matches("0x"), - 16, - ) - .ok() - }); - let arch = map - .get("CPU architecture") - .and_then(|s| s.split_whitespace().next().unwrap_or("").parse().ok()); - let rev = map - .get("CPU revision") - .and_then(|s| s.split_whitespace().next().unwrap_or("").parse().ok()); - - let m = (i << IMPLEMENTER_OFFSET) - | (var.unwrap_or(0) << VARIANT_OFFSET) - | (arch.unwrap_or(0) << ARCHITECTURE_OFFSET) - | (p << PART_OFFSET) - | rev.unwrap_or(0); - - let proc_id = map - .get("processor") - .and_then(|s| s.parse::().ok()) - .unwrap_or(idx as u32); - - cpuinfo_midrs.insert(proc_id, m); - cpuinfo_list.push(m); - } - } - - // 5. Fill any remaining gaps in sysfs_midrs from cpuinfo_midrs - for &cpu_id in &possible_cpus { - if !sysfs_midrs.contains_key(&cpu_id) - && let Some(&m) = cpuinfo_midrs.get(&cpu_id) - { - sysfs_midrs.insert(cpu_id, m); - } - } - - // 6. Return the most complete and accurate list of MIDRs - if sysfs_midrs.len() >= possible_cpus.len() && !sysfs_midrs.is_empty() { - return sysfs_midrs.into_values().collect(); - } - - if cpuinfo_list.len() >= sysfs_midrs.len() && !cpuinfo_list.is_empty() { - return cpuinfo_list; - } - - if !sysfs_midrs.is_empty() { - return sysfs_midrs.into_values().collect(); - } - - cpuinfo_list -} - -// ---------------------------------------------------------------------------- -// Feature detection via /proc/cpuinfo -// ---------------------------------------------------------------------------- - -/// Parses `/proc/cpuinfo` Features line to get a set of available features. -/// All feature names are converted to lowercase for consistency. -pub fn get_features_from_cpuinfo() -> BTreeMap { - let mut features: BTreeMap = BTreeMap::new(); - - let cpuinfo = get_proc_cpuinfo_data(); - if let Some(first) = cpuinfo.first() { - if let Some(features_str) = first.get("Features") { - for feat in features_str.split_whitespace() { - features.insert(feat.to_lowercase(), true); - } - } - } - - features -} - -// ---------------------------------------------------------------------------- -// TArmFeatures implementation -// ---------------------------------------------------------------------------- - -use crate::arm::TArmFeatures; - -impl TArmFeatures for crate::arm::ArmFeatures { - fn has_fp(&self) -> bool { - get_features_from_cpuinfo() - .get("fp") - .copied() - .unwrap_or(false) - } - - fn has_asimd(&self) -> bool { - let features = get_features_from_cpuinfo(); - features.get("asimd").copied().unwrap_or(false) - || features.get("neon").copied().unwrap_or(false) - } - - fn has_aes(&self) -> bool { - get_features_from_cpuinfo() - .get("aes") - .copied() - .unwrap_or(false) - } - - fn has_sha1(&self) -> bool { - get_features_from_cpuinfo() - .get("sha1") - .copied() - .unwrap_or(false) - } - - fn has_sha2(&self) -> bool { - get_features_from_cpuinfo() - .get("sha2") - .copied() - .unwrap_or(false) - } - - fn has_sha3(&self) -> bool { - get_features_from_cpuinfo() - .get("sha3") - .copied() - .unwrap_or(false) - } - - fn has_sha512(&self) -> bool { - get_features_from_cpuinfo() - .get("sha512") - .copied() - .unwrap_or(false) - } - - fn has_crc32(&self) -> bool { - get_features_from_cpuinfo() - .get("crc32") - .copied() - .unwrap_or(false) - } - - fn has_atomics(&self) -> bool { - let features = get_features_from_cpuinfo(); - features.get("atomics").copied().unwrap_or(false) - || features.get("lse").copied().unwrap_or(false) - } -} - -// ---------------------------------------------------------------------------- -// Get all features as a BTreeMap (for Cpu struct) -// ---------------------------------------------------------------------------- - -/// Returns all detected features as a BTreeMap of category to space-separated features. -pub fn get_all_features() -> BTreeMap<&'static str, String> { - let src = get_features_from_cpuinfo(); - let detected = crate::arm::features::populate_detected_features(&src); - crate::arm::features::build_feature_map(&detected) -} diff --git a/src/arm/os/bsd.rs b/src/arm/os/bsd.rs index 4cae64ce..1e855324 100644 --- a/src/arm/os/bsd.rs +++ b/src/arm/os/bsd.rs @@ -24,14 +24,9 @@ fn get_bsd_midrs() -> (Vec, DataSource) { let mut midrs = Vec::new(); #[cfg(target_arch = "aarch64")] - if let Some(core_ids) = core_affinity::get_core_ids() { - for core_id in core_ids { - core_affinity::set_for_current(core_id); - midrs.push(crate::arm::get_midr()); - } - } else { + crate::common::for_each_logical_core(|| { midrs.push(crate::arm::get_midr()); - } + }); #[cfg(target_arch = "arm")] panic!("Could not get midr from sysctl"); diff --git a/src/arm/os/linux.rs b/src/arm/os/linux.rs index f4147293..ec75fc19 100644 --- a/src/arm/os/linux.rs +++ b/src/arm/os/linux.rs @@ -1,8 +1,10 @@ -#![cfg(target_os = "linux")] +#![cfg(linux_os)] -//! Linux-specific ARM CPU feature detection. +//! Linux and Android ARM CPU feature detection. //! -//! Uses text-based parsing of `/proc/cpuinfo` "Features" line. +//! Uses text-based parsing of `/proc/cpuinfo` "Features" line, +//! core affinity pinning + MRS (`MIDR_EL1` via `HWCAP_CPUID` kernel trap), +//! and sysfs `/sys/devices/system/cpu/` topology and MIDR parsing. use super::OsCpuInfo; use crate::arm::brand::Vendor; @@ -11,27 +13,19 @@ use crate::common::DataSource; use crate::common::get_proc_cpuinfo_data; use std::collections::{BTreeMap, HashSet}; -/// Linux-specific CPU detection via /sys, /proc/cpuinfo, and inline asm fallback. +/// Linux and Android CPU detection via /sys, /proc/cpuinfo, and inline asm / MRS fallback. pub fn detect() -> OsCpuInfo { let mut midrs: HashSet = HashSet::new(); let mut all_midrs: Vec = Vec::new(); let mut midr_source = DataSource::CpuLookupTable; #[cfg(not(target_arch = "arm"))] - if let Some(core_ids) = core_affinity::get_core_ids() { - for core_id in core_ids { - core_affinity::set_for_current(core_id); - let midr_val = crate::arm::get_midr(); - let midr = Midr::new(midr_val); - midrs.insert(midr); - all_midrs.push(midr); - } - } else { + crate::common::for_each_logical_core(|| { let midr_val = crate::arm::get_midr(); let midr = Midr::new(midr_val); midrs.insert(midr); all_midrs.push(midr); - } + }); // Prefer sysfs for reading the MIDR on 32-bit ARM to avoid // inline asm (`mrc p15, ...`) which may cause SIGILL on @@ -92,7 +86,7 @@ pub fn detect() -> OsCpuInfo { /// Reads MIDR values from sysfs or /proc/cpuinfo across all CPU cores. /// Handles big.LITTLE / DynamIQ heterogeneous topologies and offline cores. -fn detect_linux_midrs() -> Vec { +pub fn detect_linux_midrs() -> Vec { // 1. Determine all expected CPUs from sysfs /possible or /present let mut possible_cpus = Vec::new(); if let Ok(content) = std::fs::read_to_string("/sys/devices/system/cpu/possible") { diff --git a/src/arm/os/mod.rs b/src/arm/os/mod.rs index 32d2417b..668880e9 100644 --- a/src/arm/os/mod.rs +++ b/src/arm/os/mod.rs @@ -104,21 +104,12 @@ pub mod macos; pub use macos::*; // ---------------------------------------------------------------------------- -// ! Android +// ! Linux / Android // ---------------------------------------------------------------------------- -#[cfg(target_os = "android")] -pub mod android; -#[cfg(target_os = "android")] -pub use android::*; - -// ---------------------------------------------------------------------------- -// ! Linux -// ---------------------------------------------------------------------------- - -#[cfg(target_os = "linux")] +#[cfg(linux_os)] pub mod linux; -#[cfg(target_os = "linux")] +#[cfg(linux_os)] pub use linux::*; // ---------------------------------------------------------------------------- diff --git a/src/arm/os/windows.rs b/src/arm/os/windows.rs index 370fe2f9..58bca1fd 100644 --- a/src/arm/os/windows.rs +++ b/src/arm/os/windows.rs @@ -18,20 +18,12 @@ pub fn detect() -> OsCpuInfo { let mut all_midrs: Vec = Vec::new(); let mut midr_source = DataSource::CpuLookupTable; - if let Some(core_ids) = core_affinity::get_core_ids() { - for core_id in core_ids { - core_affinity::set_for_current(core_id); - let midr_val = crate::arm::get_midr(); - let midr = Midr::new(midr_val); - midrs.insert(midr); - all_midrs.push(midr); - } - } else { + crate::common::for_each_logical_core(|| { let midr_val = crate::arm::get_midr(); let midr = Midr::new(midr_val); midrs.insert(midr); all_midrs.push(midr); - } + }); // On Windows, MRS is emulated. Try the registry for more accurate MIDRs. let windows_midrs = get_windows_midrs(); diff --git a/src/common/display.rs b/src/common/display.rs index b97638a1..26950a1f 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -291,6 +291,11 @@ impl CpuDisplay { self.display_cache_ext(cache, cache_count, l3_socket_count, None); } + pub fn display_core_cache(&self, cache: Option, core_count: u32, l3_socket_count: u32) { + let cc = |s: u32| Self::cache_count(s, core_count); + self.display_cache(cache, &cc, l3_socket_count); + } + pub fn display_cache_ext( &self, cache: Option, diff --git a/src/common/os/android.rs b/src/common/os/android.rs index f73427cb..287731f3 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -1,14 +1,14 @@ #![cfg(target_os = "android")] +use super::linux_sysfs::*; use crate::common::{ DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, cleanup_soc_vendor, is_generic_value, }; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::Path; +use std::collections::HashMap; -use crate::common::{Cache, CacheLevel, CacheType, Level1Cache}; +#[cfg(any(not(x86_cpu), test))] +use crate::common::Cache; #[cfg(any(arm_cpu, test))] use std::collections::BTreeMap; @@ -284,54 +284,8 @@ impl TOSData for OS { impl TDetect for TopologyCount { fn detect() -> Self { let sockets = OS::get_socket_count(); - - let mut topo = TopologyCount { - sockets, - ..Default::default() - }; - - let cpu_root = Path::new("/sys/devices/system/cpu"); - if cpu_root.exists() { - if let Ok(online) = fs::read_to_string(cpu_root.join("online")) { - topo.threads = parse_cpu_list_count(&online); - - let cpus = expand_cpu_list(&online); - let mut core_ids = HashSet::new(); - for cpu_id in cpus { - let core_id_path = cpu_root - .join(format!("cpu{}", cpu_id)) - .join("topology") - .join("core_id"); - if let Ok(id_str) = fs::read_to_string(&core_id_path) { - core_ids.insert(id_str.trim().to_string()); - } - } - if !core_ids.is_empty() { - topo.cores = core_ids.len() as u32; - } - } - } - - if topo.threads == 0 { - let cpuinfo = get_proc_cpuinfo_data(); - let proc_count = cpuinfo - .iter() - .filter(|m| m.contains_key("processor")) - .count() as u32; - if proc_count > 0 { - topo.threads = proc_count; - topo.cores = proc_count; - } else if let Ok(n) = std::thread::available_parallelism() { - topo.threads = n.get() as u32; - topo.cores = n.get() as u32; - } else { - topo.threads = 1; - topo.cores = 1; - } - } else if topo.cores == 0 { - topo.cores = topo.threads; - } - + let mut topo = detect_sysfs_topology(); + topo.sockets = sockets; topo } } @@ -356,170 +310,13 @@ impl Cache { } pub(crate) fn from_sys_fs() -> Option { - Self::read_cpu_cache(0) - } - - /// Read the full cache hierarchy for a single CPU from sysfs if accessible. - fn read_cpu_cache(cpu_num: u32) -> Option { - let root = Path::new("/sys/devices/system/cpu") - .join(format!("cpu{}", cpu_num)) - .join("cache"); - if !root.exists() { - return None; - } - - let mut cache = Cache { - source: DataSource::LinuxSysFs, - ..Default::default() - }; - let mut found_cache = false; - - let dir = fs::read_dir(&root).ok()?; - for entry in dir { - let entry = entry.ok()?; - let path = entry.path(); - let dir_name = entry.file_name(); - let dir_name = dir_name.to_str()?; - if !dir_name.starts_with("index") { - continue; - } - - let level_str = fs::read_to_string(path.join("level")).ok()?; - let level: u32 = level_str.trim().parse().ok()?; - - let type_str = fs::read_to_string(path.join("type")).ok()?; - let cache_type = match type_str.trim() { - "Data" => CacheType::Data, - "Instruction" => CacheType::Instruction, - "Unified" => CacheType::Unified, - _ => continue, - }; - - let size_str = fs::read_to_string(path.join("size")).ok()?; - let size_str = size_str.trim().trim_end_matches('K'); - let size_kb: u32 = size_str.parse().ok()?; - let size_bytes = size_kb * 1024; - - let assoc_str = fs::read_to_string(path.join("ways_of_associativity")).ok()?; - let assoc: u32 = assoc_str.trim().parse().unwrap_or(0); - - let share_count = - if let Ok(shared_str) = fs::read_to_string(path.join("shared_cpu_list")) { - parse_cpu_list_count(shared_str.trim()) - } else { - 0 - }; - - match level { - 1 => match cache_type { - CacheType::Unified => { - cache.l1 = Level1Cache::Unified(CacheLevel::new( - size_bytes, - cache_type, - assoc, - share_count, - )); - found_cache = true; - } - CacheType::Data => { - match &mut cache.l1 { - Level1Cache::Split { data, .. } => { - *data = CacheLevel::new(size_bytes, cache_type, assoc, share_count); - } - _ => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::new( - size_bytes, - CacheType::Data, - assoc, - share_count, - ), - instruction: CacheLevel::default(), - }; - } - } - found_cache = true; - } - CacheType::Instruction => { - match &mut cache.l1 { - Level1Cache::Split { instruction, .. } => { - *instruction = - CacheLevel::new(size_bytes, cache_type, assoc, share_count); - } - _ => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::default(), - instruction: CacheLevel::new( - size_bytes, - CacheType::Instruction, - assoc, - share_count, - ), - }; - } - } - found_cache = true; - } - _ => {} - }, - 2 => { - cache.l2 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); - found_cache = true; - } - 3 => { - cache.l3 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); - found_cache = true; - } - _ => {} - } - } - - if found_cache { Some(cache) } else { None } + read_sysfs_cpu_cache(0) } /// Read cache info for each distinct CPU type (MIDR group) from sysfs. #[cfg(any(arm_cpu, test))] pub(crate) fn from_sys_fs_per_type() -> Option> { - let cpu_root = Path::new("/sys/devices/system/cpu"); - if !cpu_root.exists() { - return None; - } - - let online = fs::read_to_string(cpu_root.join("online")).ok()?; - let cpus = expand_cpu_list(&online); - if cpus.is_empty() { - return None; - } - - let mut midr_map: BTreeMap> = BTreeMap::new(); - for &cpu_id in &cpus { - let midr_path = cpu_root - .join(format!("cpu{}", cpu_id)) - .join("regs/identification/midr_el1"); - if let Ok(content) = fs::read_to_string(&midr_path) { - if let Ok(midr) = usize::from_str_radix(content.trim().trim_start_matches("0x"), 16) - { - midr_map.entry(midr).or_default().push(cpu_id); - } - } else { - return None; - } - } - - let mut cache_map: BTreeMap = BTreeMap::new(); - for (&midr, cpus_in_group) in &midr_map { - if let Some(&first_cpu) = cpus_in_group.first() { - if let Some(cache) = Self::read_cpu_cache(first_cpu) { - cache_map.insert(midr, cache); - } - } - } - - if cache_map.is_empty() { - None - } else { - Some(cache_map) - } + read_sysfs_cache_per_type() } #[cfg(not(x86_cpu))] diff --git a/src/common/os/common.rs b/src/common/os/common.rs index 297d26a4..cd3efc38 100644 --- a/src/common/os/common.rs +++ b/src/common/os/common.rs @@ -176,6 +176,97 @@ pub fn get_proc_cpuinfo_data() -> std::vec::Vec Option { + let value = value.trim(); + if value.is_empty() { + return None; + } + + let is_ghz = value.ends_with("GHz") || value.ends_with("ghz") || value.ends_with("Ghz"); + let clean = value + .trim_end_matches("MHz") + .trim_end_matches("mhz") + .trim_end_matches("Mhz") + .trim_end_matches("GHz") + .trim_end_matches("ghz") + .trim_end_matches("Ghz") + .trim(); + + if let Some((whole, frac)) = clean.split_once('.') { + let whole_val: u64 = whole.trim().parse().ok()?; + if is_ghz { + let frac = frac.trim(); + let mut frac_mhz = 0u64; + if !frac.is_empty() { + let frac_digits = &frac[..frac.len().min(3)]; + let frac_num: u64 = frac_digits.parse().ok()?; + let mult = match frac_digits.len() { + 1 => 100, + 2 => 10, + _ => 1, + }; + frac_mhz = frac_num * mult; + } + Some(whole_val * 1000 + frac_mhz) + } else { + Some(whole_val) + } + } else if let Ok(val) = clean.parse::() { + if is_ghz { Some(val * 1000) } else { Some(val) } + } else { + None + } +} + +/// Reads a NUL-terminated or trimmed string property from device-tree (e.g. /proc/device-tree/model). +#[cfg(std_os)] +pub fn read_devicetree_string(path: impl AsRef) -> Option { + if let Ok(raw) = std::fs::read_to_string(path) { + let first = raw.split('\0').next()?.trim(); + if !first.is_empty() && !is_generic_value(first) { + return Some(first.to_string()); + } + } + None +} + +/// Reads a big-endian 32-bit or 64-bit integer (or fallback ASCII text) from device-tree. +#[cfg(std_os)] +pub fn read_devicetree_u64(path: impl AsRef) -> Option { + let p = path.as_ref(); + if let Ok(raw_bytes) = std::fs::read(p) { + if raw_bytes.len() == 4 { + let mut arr = [0u8; 4]; + arr.copy_from_slice(&raw_bytes); + return Some(u32::from_be_bytes(arr) as u64); + } else if raw_bytes.len() == 8 { + let mut arr = [0u8; 8]; + arr.copy_from_slice(&raw_bytes); + return Some(u64::from_be_bytes(arr)); + } + } + if let Ok(s) = std::fs::read_to_string(p) + && let Ok(val) = s.trim().parse::() + { + return Some(val); + } + None +} + +/// Executes a closure on each available logical processor using `core_affinity`. +#[cfg(all(std_os, not(target_arch = "arm")))] +pub fn for_each_logical_core(mut f: F) { + if let Some(core_ids) = core_affinity::get_core_ids() { + for core_id in core_ids { + core_affinity::set_for_current(core_id); + f(); + } + } else { + f(); + } +} + #[cfg(test)] mod tests { use super::*; @@ -203,4 +294,16 @@ mod tests { let single = alloc::vec!["Apple".to_string()]; assert_eq!(format_compatible_pair(single), "Apple"); } + + #[test] + fn test_parse_frequency_mhz() { + assert_eq!(parse_frequency_mhz("800 MHz"), Some(800)); + assert_eq!(parse_frequency_mhz("800MHz"), Some(800)); + assert_eq!(parse_frequency_mhz("3.2 GHz"), Some(3200)); + assert_eq!(parse_frequency_mhz("3.20 GHz"), Some(3200)); + assert_eq!(parse_frequency_mhz("2.49 GHz"), Some(2490)); + assert_eq!(parse_frequency_mhz("2400.00"), Some(2400)); + assert_eq!(parse_frequency_mhz("1500"), Some(1500)); + assert_eq!(parse_frequency_mhz(""), None); + } } diff --git a/src/common/os/linux.rs b/src/common/os/linux.rs index 16d143cf..bf825034 100644 --- a/src/common/os/linux.rs +++ b/src/common/os/linux.rs @@ -1,15 +1,17 @@ #![cfg(target_os = "linux")] +use super::linux_sysfs::*; use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, expand_cpu_list, - format_compatible_pair, get_devicetree_compatible, get_proc_cpuinfo_data, parse_cpu_list_count, + DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, format_compatible_pair, + get_devicetree_compatible, get_proc_cpuinfo_data, }; use std::collections::HashSet; -use std::fs; -use std::path::Path; #[cfg(any(not(x86_cpu), test))] -use crate::common::{Cache, CacheLevel, CacheType, Level1Cache}; +use crate::common::Cache; + +#[cfg(not(x86_cpu))] +use crate::common::{CacheLevel, CacheType, Level1Cache}; #[cfg(any(arm_cpu, test))] use std::collections::BTreeMap; @@ -207,34 +209,8 @@ impl TOSData for OS { impl TDetect for TopologyCount { fn detect() -> Self { let sockets = OS::get_socket_count(); - - let mut topo = TopologyCount { - sockets, - ..Default::default() - }; - - let cpu_root = Path::new("/sys/devices/system/cpu"); - if !cpu_root.exists() { - return topo; - } - - if let Ok(online) = fs::read_to_string(cpu_root.join("online")) { - topo.threads = parse_cpu_list_count(&online); - - let cpus = expand_cpu_list(&online); - let mut core_ids = std::collections::HashSet::new(); - for cpu_id in cpus { - let core_id_path = cpu_root - .join(format!("cpu{}", cpu_id)) - .join("topology") - .join("core_id"); - if let Ok(id_str) = fs::read_to_string(&core_id_path) { - core_ids.insert(id_str.trim().to_string()); - } - } - topo.cores = core_ids.len() as u32; - } - + let mut topo = detect_sysfs_topology(); + topo.sockets = sockets; topo } } @@ -256,179 +232,13 @@ impl Cache { #[cfg(not(x86_cpu))] pub(crate) fn from_sys_fs() -> Option { - Self::read_cpu_cache(0) - } - - /// Read the full cache hierarchy for a single CPU from sysfs. - fn read_cpu_cache(cpu_num: u32) -> Option { - let root = Path::new("/sys/devices/system/cpu") - .join(format!("cpu{}", cpu_num)) - .join("cache"); - if !root.exists() { - return None; - } - - let mut cache = Cache { - source: DataSource::LinuxSysFs, - ..Default::default() - }; - let mut found_cache = false; - - let dir = fs::read_dir(&root).ok()?; - for entry in dir { - let entry = entry.ok()?; - let path = entry.path(); - let dir_name = entry.file_name(); - let dir_name = dir_name.to_str()?; - if !dir_name.starts_with("index") { - continue; - } - - let level_str = fs::read_to_string(path.join("level")).ok()?; - let level: u32 = level_str.trim().parse().ok()?; - - let type_str = fs::read_to_string(path.join("type")).ok()?; - let cache_type = match type_str.trim() { - "Data" => CacheType::Data, - "Instruction" => CacheType::Instruction, - "Unified" => CacheType::Unified, - _ => continue, - }; - - let size_str = fs::read_to_string(path.join("size")).ok()?; - let size_str = size_str.trim().trim_end_matches('K'); - let size_kb: u32 = size_str.parse().ok()?; - let size_bytes = size_kb * 1024; - - let assoc_str = fs::read_to_string(path.join("ways_of_associativity")).ok()?; - let assoc: u32 = assoc_str.trim().parse().unwrap_or(0); - - let share_count = - if let Ok(shared_str) = fs::read_to_string(path.join("shared_cpu_list")) { - parse_cpu_list_count(shared_str.trim()) - } else { - 0 - }; - - match level { - 1 => match cache_type { - CacheType::Unified => { - cache.l1 = Level1Cache::Unified(CacheLevel::new( - size_bytes, - cache_type, - assoc, - share_count, - )); - found_cache = true; - } - CacheType::Data => { - match &mut cache.l1 { - Level1Cache::Split { data, .. } => { - *data = CacheLevel::new(size_bytes, cache_type, assoc, share_count); - } - _ => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::new( - size_bytes, - CacheType::Data, - assoc, - share_count, - ), - instruction: CacheLevel::default(), - }; - } - } - found_cache = true; - } - CacheType::Instruction => { - match &mut cache.l1 { - Level1Cache::Split { instruction, .. } => { - *instruction = - CacheLevel::new(size_bytes, cache_type, assoc, share_count); - } - _ => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::default(), - instruction: CacheLevel::new( - size_bytes, - CacheType::Instruction, - assoc, - share_count, - ), - }; - } - } - found_cache = true; - } - _ => {} - }, - 2 => { - cache.l2 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); - found_cache = true; - } - 3 => { - cache.l3 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); - found_cache = true; - } - _ => {} - } - } - - if found_cache { Some(cache) } else { None } + read_sysfs_cpu_cache(0) } /// Read cache info for each distinct CPU type (MIDR group). - /// - /// On heterogeneous ARM systems (big.LITTLE / DynamIQ), each core type may - /// have a different cache hierarchy. This method reads per-CPU cache info - /// from sysfs and returns a map keyed by MIDR value. - /// - /// Returns `None` if `midr_el1` is unavailable (non-ARM or older kernel). #[cfg(any(arm_cpu, test))] pub(crate) fn from_sys_fs_per_type() -> Option> { - let cpu_root = Path::new("/sys/devices/system/cpu"); - if !cpu_root.exists() { - return None; - } - - let online = fs::read_to_string(cpu_root.join("online")).ok()?; - let cpus = expand_cpu_list(&online); - if cpus.is_empty() { - return None; - } - - // Read MIDRs for all online CPUs, group by value - let mut midr_map: BTreeMap> = BTreeMap::new(); - for &cpu_id in &cpus { - let midr_path = cpu_root - .join(format!("cpu{}", cpu_id)) - .join("regs/identification/midr_el1"); - if let Ok(content) = fs::read_to_string(&midr_path) { - if let Ok(midr) = usize::from_str_radix(content.trim().trim_start_matches("0x"), 16) - { - midr_map.entry(midr).or_default().push(cpu_id); - } - } else { - // No midr_el1 → not an ARM system, can't do per-type - return None; - } - } - - // Read cache config from first CPU of each MIDR group - let mut cache_map: BTreeMap = BTreeMap::new(); - for (&midr, cpus_in_group) in &midr_map { - if let Some(&first_cpu) = cpus_in_group.first() - && let Some(cache) = Self::read_cpu_cache(first_cpu) - { - cache_map.insert(midr, cache); - } - } - - if cache_map.is_empty() { - None - } else { - Some(cache_map) - } + read_sysfs_cache_per_type() } #[cfg(not(x86_cpu))] @@ -531,6 +341,7 @@ impl Cache { mod tests { use super::super::normalize_for_compare; use super::*; + use crate::common::{expand_cpu_list, parse_cpu_list_count}; #[test] fn test_parse_cpu_list_count_single() { diff --git a/src/common/os/linux_sysfs.rs b/src/common/os/linux_sysfs.rs new file mode 100644 index 00000000..8f663e9a --- /dev/null +++ b/src/common/os/linux_sysfs.rs @@ -0,0 +1,215 @@ +#![cfg(linux_os)] + +use crate::common::{ + Cache, CacheLevel, CacheType, DataSource, Level1Cache, TopologyCount, expand_cpu_list, + get_proc_cpuinfo_data, parse_cpu_list_count, +}; +use std::collections::{BTreeMap, HashSet}; +use std::fs; +use std::path::Path; + +/// Detects topology counts (threads and cores) from `/sys/devices/system/cpu`. +pub fn detect_sysfs_topology() -> TopologyCount { + let mut topo = TopologyCount::default(); + + let cpu_root = Path::new("/sys/devices/system/cpu"); + if cpu_root.exists() + && let Ok(online) = fs::read_to_string(cpu_root.join("online")) + { + topo.threads = parse_cpu_list_count(&online); + + let cpus = expand_cpu_list(&online); + let mut core_ids = HashSet::new(); + for cpu_id in cpus { + let core_id_path = cpu_root + .join(format!("cpu{}", cpu_id)) + .join("topology") + .join("core_id"); + if let Ok(id_str) = fs::read_to_string(&core_id_path) { + core_ids.insert(id_str.trim().to_string()); + } + } + if !core_ids.is_empty() { + topo.cores = core_ids.len() as u32; + } + } + + if topo.threads == 0 { + let cpuinfo = get_proc_cpuinfo_data(); + let proc_count = cpuinfo + .iter() + .filter(|m| m.contains_key("processor")) + .count() as u32; + if proc_count > 0 { + topo.threads = proc_count; + topo.cores = proc_count; + } else if let Ok(n) = std::thread::available_parallelism() { + topo.threads = n.get() as u32; + topo.cores = n.get() as u32; + } else { + topo.threads = 1; + topo.cores = 1; + } + } else if topo.cores == 0 { + topo.cores = topo.threads; + } + + topo +} + +/// Read the full cache hierarchy for a single CPU from sysfs if accessible. +pub fn read_sysfs_cpu_cache(cpu_num: u32) -> Option { + let root = Path::new("/sys/devices/system/cpu") + .join(format!("cpu{}", cpu_num)) + .join("cache"); + if !root.exists() { + return None; + } + + let mut cache = Cache { + source: DataSource::LinuxSysFs, + ..Default::default() + }; + let mut found_cache = false; + + let dir = fs::read_dir(&root).ok()?; + for entry in dir { + let entry = entry.ok()?; + let path = entry.path(); + let dir_name = entry.file_name(); + let dir_name = dir_name.to_str()?; + if !dir_name.starts_with("index") { + continue; + } + + let level_str = fs::read_to_string(path.join("level")).ok()?; + let level: u32 = level_str.trim().parse().ok()?; + + let type_str = fs::read_to_string(path.join("type")).ok()?; + let cache_type = match type_str.trim() { + "Data" => CacheType::Data, + "Instruction" => CacheType::Instruction, + "Unified" => CacheType::Unified, + _ => continue, + }; + + let size_str = fs::read_to_string(path.join("size")).ok()?; + let size_str = size_str.trim().trim_end_matches('K'); + let size_kb: u32 = size_str.parse().ok()?; + let size_bytes = size_kb * 1024; + + let assoc_str = fs::read_to_string(path.join("ways_of_associativity")).ok()?; + let assoc: u32 = assoc_str.trim().parse().unwrap_or(0); + + let share_count = if let Ok(shared_str) = fs::read_to_string(path.join("shared_cpu_list")) { + parse_cpu_list_count(shared_str.trim()) + } else { + 0 + }; + + match level { + 1 => match cache_type { + CacheType::Unified => { + cache.l1 = Level1Cache::Unified(CacheLevel::new( + size_bytes, + cache_type, + assoc, + share_count, + )); + found_cache = true; + } + CacheType::Data => { + match &mut cache.l1 { + Level1Cache::Split { data, .. } => { + *data = CacheLevel::new(size_bytes, cache_type, assoc, share_count); + } + _ => { + cache.l1 = Level1Cache::Split { + data: CacheLevel::new(size_bytes, cache_type, assoc, share_count), + instruction: CacheLevel::default(), + }; + } + } + found_cache = true; + } + CacheType::Instruction => { + match &mut cache.l1 { + Level1Cache::Split { instruction, .. } => { + *instruction = + CacheLevel::new(size_bytes, cache_type, assoc, share_count); + } + _ => { + cache.l1 = Level1Cache::Split { + data: CacheLevel::default(), + instruction: CacheLevel::new( + size_bytes, + cache_type, + assoc, + share_count, + ), + }; + } + } + found_cache = true; + } + _ => {} + }, + 2 => { + cache.l2 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); + found_cache = true; + } + 3 => { + cache.l3 = Some(CacheLevel::new(size_bytes, cache_type, assoc, share_count)); + found_cache = true; + } + _ => {} + } + } + + if found_cache { Some(cache) } else { None } +} + +/// Read cache info for each distinct CPU type (MIDR group) on heterogeneous ARM systems. +pub fn read_sysfs_cache_per_type() -> Option> { + let cpu_root = Path::new("/sys/devices/system/cpu"); + if !cpu_root.exists() { + return None; + } + + let online = fs::read_to_string(cpu_root.join("online")).ok()?; + let cpus = expand_cpu_list(&online); + if cpus.is_empty() { + return None; + } + + // Read MIDRs for all online CPUs, group by value + let mut midr_map: BTreeMap> = BTreeMap::new(); + for &cpu_id in &cpus { + let midr_path = cpu_root + .join(format!("cpu{}", cpu_id)) + .join("regs/identification/midr_el1"); + if let Ok(content) = fs::read_to_string(&midr_path) { + if let Ok(midr) = usize::from_str_radix(content.trim().trim_start_matches("0x"), 16) { + midr_map.entry(midr).or_default().push(cpu_id); + } + } else { + return None; + } + } + + // Read cache config from first CPU of each MIDR group + let mut cache_map: BTreeMap = BTreeMap::new(); + for (&midr, cpus_in_group) in &midr_map { + if let Some(&first_cpu) = cpus_in_group.first() + && let Some(cache) = read_sysfs_cpu_cache(first_cpu) + { + cache_map.insert(midr, cache); + } + } + + if cache_map.is_empty() { + None + } else { + Some(cache_map) + } +} diff --git a/src/common/os/mod.rs b/src/common/os/mod.rs index 15cabafb..cc520b63 100644 --- a/src/common/os/mod.rs +++ b/src/common/os/mod.rs @@ -28,10 +28,16 @@ pub mod haiku; #[cfg(windows_os)] pub mod windows; +#[cfg(linux_os)] +pub mod linux_sysfs; + // ---------------------------------------------------------------------------- pub use common::*; +#[cfg(linux_os)] +pub use linux_sysfs::*; + #[cfg(target_os = "android")] pub use android::*; diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index 31f8d1a6..32be97e3 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -82,14 +82,12 @@ impl Cpu { return None; } - if let Ok(freq_str) = fs::read_to_string(dt_root.join("clock-frequency")) - && let Ok(freq_hz) = freq_str.trim().parse::() - { + if let Some(freq_hz) = crate::common::read_devicetree_u64(dt_root.join("clock-frequency")) { return Some(freq_hz / 1_000_000); } - if let Ok(freq_str) = fs::read_to_string(dt_root.join("timebase-frequency")) - && let Ok(freq_hz) = freq_str.trim().parse::() + if let Some(freq_hz) = + crate::common::read_devicetree_u64(dt_root.join("timebase-frequency")) { return Some(freq_hz / 1_000_000); } @@ -111,7 +109,7 @@ impl Cpu { for line in output_str.lines() { if (line.starts_with("CPU max MHz") || line.starts_with("CPU MHz")) && let Some(value) = line.split(':').nth(1) - && let Some(freq) = Self::parse_mhz_value(value) + && let Some(freq) = crate::common::parse_frequency_mhz(value) { return Some(freq); } @@ -125,7 +123,7 @@ impl Cpu { let cpuinfo = get_proc_cpuinfo_data(); for map in &cpuinfo { if let Some(val) = map.get("cpu MHz").or_else(|| map.get("clock")) - && let Some(freq) = Self::parse_mhz_value(val) + && let Some(freq) = crate::common::parse_frequency_mhz(val) { return Some(freq); } @@ -138,24 +136,6 @@ impl Cpu { fn detect_clock_speed_from_cpuinfo() -> Option { None } - - fn parse_mhz_value(value: &str) -> Option { - let value = value.trim(); - let value = value.trim_end_matches("MHz").trim().trim_end_matches("MHz"); - let value = value.trim_end_matches("GHz"); - - if value.contains('.') { - let parts: Vec<&str> = value.split('.').collect(); - if let Ok(mhz) = parts[0].parse::() { - if value.ends_with("GHz") { - return Some(mhz * 1000); - } - return Some(mhz); - } - } - - value.parse::().ok() - } } impl TDetect for Cpu { diff --git a/src/ppc/display.rs b/src/ppc/display.rs index c4396c32..5bc73f72 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -33,8 +33,7 @@ impl TCpuDisplay for Cpu { if let Some(core) = self.cores.first() { disp.display_frequency(core.speed, flags); - let cc = |s| CpuDisplay::cache_count(s, total_cores); - disp.display_cache(core.cache, &cc, 0); + disp.display_core_cache(core.cache, total_cores, 0); } println!(); diff --git a/src/riscv/display.rs b/src/riscv/display.rs index f3e49e3f..b1b03653 100644 --- a/src/riscv/display.rs +++ b/src/riscv/display.rs @@ -61,8 +61,7 @@ impl CpuDisplay { }, ); - let cc = |s| CpuDisplay::cache_count(s, core.count); - disp.display_cache(core.cache, &cc, 0); + disp.display_core_cache(core.cache, core.count, 0); if core.cache.is_none() { disp.newline(); @@ -71,9 +70,7 @@ impl CpuDisplay { } else if let Some(core) = cpu_info.cores.first() { disp.display_topology_line(core.count, core.threads, false, 1); - let cc = - |share_count: u32| -> String { CpuDisplay::cache_count(share_count, core.count) }; - disp.display_cache(core.cache, &cc, 0); + disp.display_core_cache(core.cache, core.count, 0); disp.display_frequency(core.speed, flags); } From fe5591e0fa8cf499660341420a4861b064b4cbdb Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 14:35:26 -0400 Subject: [PATCH 21/30] Fix speed and topology detection for PowerPC --- src/ppc/cpu.rs | 139 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 23 deletions(-) diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index 32be97e3..e75c6b33 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -26,7 +26,17 @@ impl Cpu { #[cfg(target_os = "linux")] { let cpuinfo = get_proc_cpuinfo_data(); - let thread_count = cpuinfo.len().max(1) as u32; + let proc_count = cpuinfo + .iter() + .filter(|m| m.contains_key("processor")) + .count() as u32; + + let thread_count = if proc_count > 0 { + proc_count + } else { + let topo = crate::common::detect_sysfs_topology(); + if topo.threads > 0 { topo.threads } else { 1 } + }; // Check sysfs for SMT thread siblings per core let path = "/sys/devices/system/cpu/cpu0/topology/thread_siblings_list"; @@ -56,40 +66,68 @@ impl Cpu { } fn detect_clock_speed() -> (Option, DataSource) { - // Try to get clock speed from device tree first + // 1. Try /proc/cpuinfo first ("clock" or "cpu MHz") + if let Some(speed) = Self::detect_clock_speed_from_cpuinfo() { + return (Some(speed), DataSource::LinuxProcCpuinfo); + } + + // 2. Try sysfs cpufreq + if let Some(speed) = Self::detect_clock_speed_from_cpufreq() { + return (Some(speed), DataSource::LinuxSysFs); + } + + // 3. Try device tree CPU nodes (/proc/device-tree/cpus/*/clock-frequency) if let Some(speed) = Self::detect_clock_speed_from_device_tree() { return (Some(speed), DataSource::DeviceTree); } - // Try lscpu for clock speed + // 4. Try lscpu for clock speed if let Some(speed) = Self::detect_clock_speed_from_lscpu() { return (Some(speed), DataSource::Lscpu); } - // Fallback to /proc/cpuinfo - let speed = Self::detect_clock_speed_from_cpuinfo(); - let source = if speed.is_some() { - DataSource::LinuxProcCpuinfo - } else { - DataSource::DefaultValue - }; - (speed, source) + (None, DataSource::DefaultValue) } - fn detect_clock_speed_from_device_tree() -> Option { - let dt_root = Path::new("/proc/device-tree"); - if !dt_root.exists() { - return None; - } + fn detect_clock_speed_from_cpufreq() -> Option { + let paths = [ + "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq", + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq", + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", + ]; - if let Some(freq_hz) = crate::common::read_devicetree_u64(dt_root.join("clock-frequency")) { - return Some(freq_hz / 1_000_000); + for path in paths { + if let Ok(content) = fs::read_to_string(path) + && let Ok(khz) = content.trim().parse::() + { + let mhz = khz / 1000; + if mhz > 0 { + return Some(mhz); + } + } } - if let Some(freq_hz) = - crate::common::read_devicetree_u64(dt_root.join("timebase-frequency")) - { - return Some(freq_hz / 1_000_000); + None + } + + fn detect_clock_speed_from_device_tree() -> Option { + let cpus_roots = [ + Path::new("/proc/device-tree/cpus"), + Path::new("/sys/firmware/devicetree/base/cpus"), + ]; + + for cpus_dir in cpus_roots { + if let Ok(entries) = fs::read_dir(cpus_dir) { + for entry in entries.flatten() { + let path = entry.path(); + let clock_path = path.join("clock-frequency"); + if let Some(freq_hz) = crate::common::read_devicetree_u64(&clock_path) + && freq_hz > 0 + { + return Some(freq_hz / 1_000_000); + } + } + } } None @@ -122,7 +160,7 @@ impl Cpu { fn detect_clock_speed_from_cpuinfo() -> Option { let cpuinfo = get_proc_cpuinfo_data(); for map in &cpuinfo { - if let Some(val) = map.get("cpu MHz").or_else(|| map.get("clock")) + if let Some(val) = map.get("clock").or_else(|| map.get("cpu MHz")) && let Some(freq) = crate::common::parse_frequency_mhz(val) { return Some(freq); @@ -196,3 +234,58 @@ impl TDetect for Cpu { } } } + +#[cfg(test)] +mod tests { + + #[test] + fn test_parse_ppc_clock_speed() { + assert_eq!( + crate::common::parse_frequency_mhz("1250.000000MHz"), + Some(1250) + ); + assert_eq!( + crate::common::parse_frequency_mhz("166.666666MHz"), + Some(166) + ); + assert_eq!(crate::common::parse_frequency_mhz("1.25GHz"), Some(1250)); + assert_eq!(crate::common::parse_frequency_mhz("1.25 GHz"), Some(1250)); + assert_eq!(crate::common::parse_frequency_mhz("1.42 GHz"), Some(1420)); + assert_eq!(crate::common::parse_frequency_mhz("800 MHz"), Some(800)); + assert_eq!(crate::common::parse_frequency_mhz("1600.00"), Some(1600)); + } + + #[test] + fn test_ppc_cpuinfo_processor_filter() { + let cpuinfo_sample = "processor\t: 0\ncpu\t\t: 7447/7457\nclock\t\t: 1250.000000MHz\nrevision\t: 1.1 (pvr 8002 0101)\n\nplatform\t: PowerBook\nmodel\t\t: PowerBook5,2\nmachine\t\t: PowerBook5,2\n"; + let sections: Vec> = cpuinfo_sample + .split("\n\n") + .filter(|s| !s.trim().is_empty()) + .map(|section| { + let mut map = std::collections::HashMap::new(); + for line in section.lines() { + if let Some((key, val)) = line.split_once(':') { + map.insert(key.trim().to_string(), val.trim().to_string()); + } + } + map + }) + .collect(); + + // 2 sections total: 1 processor section, 1 platform/system section + assert_eq!(sections.len(), 2); + + // Processor section count should be exactly 1 + let proc_count = sections + .iter() + .filter(|m| m.contains_key("processor")) + .count(); + assert_eq!(proc_count, 1); + + // Clock speed from processor section + let clock = sections[0] + .get("clock") + .and_then(|v| crate::common::parse_frequency_mhz(v)); + assert_eq!(clock, Some(1250)); + } +} From f0146abdc84a5e1db9227eddcd76450093776f5b Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 15:20:02 -0400 Subject: [PATCH 22/30] Share topology struct between architectures --- src/arm/cpu.rs | 11 + src/arm/display.rs | 18 +- src/common/cpu.rs | 170 ++++++++++++++ src/common/display.rs | 65 +++++- src/common/mod.rs | 422 +---------------------------------- src/common/os/linux_sysfs.rs | 36 ++- src/common/topology.rs | 171 ++++++++++++++ src/common/util.rs | 95 ++++++++ src/ppc/cpu.rs | 36 ++- src/ppc/display.rs | 19 +- src/ppc/micro_arch.rs | 3 + src/riscv/cpu.rs | 11 + src/riscv/display.rs | 9 +- src/x86/cpu.rs | 15 +- src/x86/display.rs | 46 ++-- src/x86/dos/mod.rs | 18 +- src/x86/efi/mod.rs | 16 +- src/x86/topology.rs | 30 +-- 18 files changed, 656 insertions(+), 535 deletions(-) create mode 100644 src/common/cpu.rs create mode 100644 src/common/topology.rs create mode 100644 src/common/util.rs diff --git a/src/arm/cpu.rs b/src/arm/cpu.rs index 8faa8b33..635a0c74 100644 --- a/src/arm/cpu.rs +++ b/src/arm/cpu.rs @@ -31,10 +31,21 @@ impl TDetect for Cpu { features_source: info.features_source, }; + let sockets = OS::get_socket_count(); + let total_cores = info.cores.iter().map(|c| c.count).sum(); + let total_threads = info.cores.iter().map(|c| c.threads).sum(); + let topology = Topology { + sockets, + cores: TopologyTier::new(total_cores, sockets.source), + threads: TopologyTier::new(total_threads, sockets.source), + ..Default::default() + }; + Self { system: OS::get_system_name(), vendor: info.vendor, model: info.model, + topology, cores: info.cores, features, extra, diff --git a/src/arm/display.rs b/src/arm/display.rs index 83d26a3d..720f3d01 100644 --- a/src/arm/display.rs +++ b/src/arm/display.rs @@ -174,6 +174,20 @@ impl CpuDisplay { disp.simple_line_opt("Process", cpu_info.cpu_arch.technology); + let total_cores = cpu_info.total_cores(); + let total_threads = cpu_info.total_threads(); + let sockets = cpu_info.total_sockets(); + + if sockets > 1 || flags.verbose { + disp.display_topology_line( + sockets, + total_cores, + total_threads, + cpu_info.is_hybrid(), + cpu_info.cores.len(), + ); + } + if cpu_info.is_hybrid() { for (i, core) in cpu_info.cores.iter().enumerate() { disp.core_heading(i); @@ -202,7 +216,7 @@ impl CpuDisplay { }, ); - disp.display_core_cache(core.cache, core.count, 0); + disp.display_core_cache(core.cache, core.count, sockets); if core.cache.is_none() { disp.newline(); @@ -226,7 +240,7 @@ impl CpuDisplay { disp.display_frequency(core.speed, flags); - disp.display_core_cache(core.cache, core.count, 0); + disp.display_core_cache(core.cache, core.count, sockets); } // Display features diff --git a/src/common/cpu.rs b/src/common/cpu.rs new file mode 100644 index 00000000..f8b43ad4 --- /dev/null +++ b/src/common/cpu.rs @@ -0,0 +1,170 @@ +use crate::common::cache::Cache; +use crate::common::topology::{Speed, Topology}; +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; + +pub trait TDetect { + fn detect() -> Self; +} + +#[derive(Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)] +pub enum CoreType { + Super, + #[default] + Performance, + Efficiency, +} + +impl From<&str> for CoreType { + fn from(val: &str) -> Self { + match val { + "Super" => CoreType::Super, + "Performance" => CoreType::Performance, + "Efficiency" => CoreType::Efficiency, + _ => CoreType::Performance, + } + } +} + +impl From for &str { + fn from(val: CoreType) -> &'static str { + match val { + CoreType::Super => "Super", + CoreType::Performance => "Performance", + CoreType::Efficiency => "Efficiency", + } + } +} + +impl From for CoreType { + fn from(val: String) -> Self { + Self::from(val.as_str()) + } +} + +/// Information about a specific core type/cluster in the CPU. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct CpuCore { + /// Classification of this core (Performance, Efficiency, Super) + pub kind: CoreType, + /// Microarchitecture variant of this core type + pub micro_arch: M, + /// Marketing or core codename (e.g., "Golden Cove", "Cortex-A78", "U74") + pub name: Option, + /// Core implementer / designer (e.g., "ARM", "Nvidia", "Apple") + pub implementer: Option, + /// Cache hierarchy specific to this core cluster + pub cache: Option, + /// Clock speed for this specific core cluster (base and boost frequencies in MHz) + pub speed: Option, + /// Number of physical cores in this cluster + pub count: u32, + /// Number of logical threads in this cluster + pub threads: u32, +} + +/// Unified CPU representation across all hardware architectures. +#[derive(Debug, Default, PartialEq)] +pub struct Cpu { + /// The system name, if applicable + pub system: Option, + /// CPU vendor name + pub vendor: String, + /// CPU model name + pub model: String, + /// CPU topology details (sockets, dies, cores, threads, speed, cache) + pub topology: Topology, + /// Per-core-cluster breakdown of CPU cores + pub cores: Vec>, + /// Detected CPU features + pub features: BTreeMap<&'static str, String>, + /// Architecture-specific extension data + pub extra: E, +} + +impl Cpu { + /// Total sockets (at least 1) + pub fn total_sockets(&self) -> u32 { + self.topology.sockets.count.max(1) + } + + /// Returns true if this CPU has multiple core types (hybrid architecture). + pub fn is_hybrid(&self) -> bool { + self.cores.len() > 1 + } + + /// Total physical cores across all clusters + pub fn total_cores(&self) -> u32 { + let sum: u32 = self.cores.iter().map(|c| c.count).sum(); + if sum > 0 { + sum + } else { + self.topology.cores.count.max(1) + } + } + + /// Total logical threads across all clusters + pub fn total_threads(&self) -> u32 { + let sum: u32 = self.cores.iter().map(|c| c.threads).sum(); + if sum > 0 { + sum + } else { + self.topology.threads.count.max(1) + } + } +} + +impl core::ops::Deref for Cpu { + type Target = E; + fn deref(&self) -> &Self::Target { + &self.extra + } +} + +impl core::ops::DerefMut for Cpu { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.extra + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_core_type_from_str_performance() { + assert_eq!(CoreType::from("Performance"), CoreType::Performance); + } + + #[test] + fn test_core_type_from_str_efficiency() { + assert_eq!(CoreType::from("Efficiency"), CoreType::Efficiency); + } + + #[test] + fn test_core_type_from_str_super() { + assert_eq!(CoreType::from("Super"), CoreType::Super); + } + + #[test] + fn test_core_type_from_str_unknown_defaults_to_performance() { + assert_eq!(CoreType::from("Unknown"), CoreType::Performance); + } + + #[test] + fn test_core_type_from_string() { + let s = String::from("Efficiency"); + assert_eq!(CoreType::from(s), CoreType::Efficiency); + } + + #[test] + fn test_core_type_into_str() { + let s: &str = CoreType::Super.into(); + assert_eq!(s, "Super"); + let s: &str = CoreType::Performance.into(); + assert_eq!(s, "Performance"); + let s: &str = CoreType::Efficiency.into(); + assert_eq!(s, "Efficiency"); + } +} diff --git a/src/common/display.rs b/src/common/display.rs index 26950a1f..a74a1bc1 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -5,9 +5,23 @@ use super::constants::*; use alloc::format; use alloc::string::String; -use crate::common::CliFlags; use crate::println; +#[derive(Debug, Default, Clone, Copy)] +pub struct CliFlags { + pub compact: bool, + pub color: bool, + pub verbose: bool, +} + +pub trait TCpuDisplay: super::cpu::TDetect { + /// Display the Rust debug output of the CPU object + fn debug(&self); + + /// Display the CPU information in a table format + fn display_table(&self, flags: CliFlags); +} + pub struct CpuDisplay { pub flags: CliFlags, } @@ -221,25 +235,45 @@ impl CpuDisplay { /// Displays the Topology line for homogeneous or hybrid configurations. pub fn display_topology_line( &self, + sockets: u32, total_cores: u32, total_threads: u32, is_hybrid: bool, cluster_count: usize, ) { + let sockets = sockets.max(1); if is_hybrid { + let socket_prefix = if sockets > 1 { + let socket_str = Self::plural(sockets, "socket", "sockets"); + alloc::format!("{sockets} {socket_str}, ") + } else { + alloc::string::String::new() + }; self.simple_line( "Topology", &alloc::format!( - "{} across {} core types", + "{}{} across {} core types", + socket_prefix, Self::format_core_threads(total_cores, total_threads), cluster_count ), ); } else if total_cores > 0 { - self.simple_line( - "Topology", - &Self::format_core_threads(total_cores, total_threads), - ); + if sockets > 1 || self.flags.verbose { + let socket_str = Self::plural(sockets, "socket", "sockets"); + let core_str = Self::plural(total_cores, "core", "cores"); + let thread_str = Self::plural(total_threads, "thread", "threads"); + + self.simple_line( + "Topology", + &alloc::format!("{sockets} {socket_str}, {total_cores} {core_str}, {total_threads} {thread_str}"), + ); + } else { + self.simple_line( + "Topology", + &Self::format_core_threads(total_cores, total_threads), + ); + } } } @@ -924,4 +958,23 @@ mod tests { disp.display_with_raw("System", "MacBook Pro", Some("MacBookPro18,1"), false); disp.display_with_raw("System", "MacBook Pro", Some("MacBookPro18,1"), true); } + + #[test] + fn test_cli_flags_default() { + let f = CliFlags::default(); + assert!(!f.color); + assert!(!f.verbose); + } + + #[test] + fn test_cli_flags_explicit() { + let f = CliFlags { + compact: true, + color: true, + verbose: true, + }; + assert!(f.compact); + assert!(f.color); + assert!(f.verbose); + } } diff --git a/src/common/mod.rs b/src/common/mod.rs index 67728050..f024f7db 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,425 +1,15 @@ pub mod cache; - pub mod constants; - +pub mod cpu; pub mod display; - pub mod os; +pub mod topology; +pub mod util; pub use cache::*; - pub use constants::*; - +pub use cpu::*; pub use display::*; - pub use os::*; - -use alloc::string::String; - -pub fn ucfirst(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } -} - -pub fn cleanup_soc_vendor(s: &str) -> String { - let lower = s.to_lowercase(); - let vendor = match lower.as_str() { - "allwinner" | "sunxi" => "Allwinner", - "amlogic" | "meson" => "Amlogic", - "apple" => "Apple", - "bigtreetech" => "BigTreeTech", - "brcm" | "broadcom" => "Broadcom", - "hisilicon" | "hi" => "HiSilicon", - "mediatek" | "mtk" => "MediaTek", - "nxp" | "freescale" => "NXP", - "qcom" | "qualcomm" => "Qualcomm", - "raspberrypi" => "Raspberry Pi", - "realtek" => "Realtek", - "renesas" => "Renesas", - "rk" | "rockchip" => "Rockchip", - "samsung" | "exynos" => "Samsung", - "st" | "stmicro" => "STMicroelectronics", - "ti" => "Texas Instruments", - "xilinx" => "Xilinx", - _ => return ucfirst(s), - }; - - String::from(vendor) -} - -#[derive(Debug, Default, Clone, Copy)] -pub struct CliFlags { - pub compact: bool, - pub color: bool, - pub verbose: bool, -} - -pub trait TDetect { - fn detect() -> Self; -} - -pub trait TCpuDisplay: TDetect { - /// Display the Rust debug output of the CPU object - fn debug(&self); - - /// Display the CPU information in a table format - fn display_table(&self, flags: CliFlags); -} - -#[derive(Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy, Clone)] -pub enum CoreType { - Super, - #[default] - Performance, - Efficiency, -} - -impl From<&str> for CoreType { - fn from(val: &str) -> Self { - match val { - "Super" => CoreType::Super, - "Performance" => CoreType::Performance, - "Efficiency" => CoreType::Efficiency, - _ => CoreType::Performance, - } - } -} - -impl From for &str { - fn from(val: CoreType) -> &'static str { - match val { - CoreType::Super => "Super", - CoreType::Performance => "Performance", - CoreType::Efficiency => "Efficiency", - } - } -} - -impl From for CoreType { - fn from(val: String) -> Self { - Self::from(val.as_str()) - } -} - -/// CPU speed information (base and boost frequencies). -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub struct Speed { - /// Base frequency in MHz - pub base: u32, - /// Boost frequency in MHz - pub boost: u32, - /// Whether the frequency was measured (vs reported by CPU) - pub measured: bool, -} - -/// Information about a specific core type/cluster in the CPU. -#[derive(Debug, Default, Clone, PartialEq)] -pub struct CpuCore { - /// Classification of this core (Performance, Efficiency, Super) - pub kind: CoreType, - /// Microarchitecture variant of this core type - pub micro_arch: M, - /// Marketing or core codename (e.g., "Golden Cove", "Cortex-A78", "U74") - pub name: Option, - /// Core implementer / designer (e.g., "ARM", "Nvidia", "Apple") - pub implementer: Option, - /// Cache hierarchy specific to this core cluster - pub cache: Option, - /// Clock speed for this specific core cluster (base and boost frequencies in MHz) - pub speed: Option, - /// Number of physical cores in this cluster - pub count: u32, - /// Number of logical threads in this cluster - pub threads: u32, -} - -/// Unified CPU representation across all hardware architectures. -#[derive(Debug, Default, PartialEq)] -pub struct Cpu { - /// The system name, if applicable - pub system: Option, - /// CPU vendor name - pub vendor: String, - /// CPU model name - pub model: String, - /// Per-core-cluster breakdown of CPU cores - pub cores: alloc::vec::Vec>, - /// Detected CPU features - pub features: alloc::collections::BTreeMap<&'static str, String>, - /// Architecture-specific extension data - pub extra: E, -} - -impl Cpu { - /// Returns true if this CPU has multiple core types (hybrid architecture). - pub fn is_hybrid(&self) -> bool { - self.cores.len() > 1 - } - - /// Total physical cores across all clusters - pub fn total_cores(&self) -> u32 { - self.cores.iter().map(|c| c.count).sum() - } - - /// Total logical threads across all clusters - pub fn total_threads(&self) -> u32 { - self.cores.iter().map(|c| c.threads).sum() - } -} - -impl core::ops::Deref for Cpu { - type Target = E; - fn deref(&self) -> &Self::Target { - &self.extra - } -} - -impl core::ops::DerefMut for Cpu { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.extra - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct TopologyTier { - pub count: u32, - pub source: DataSource, -} - -impl TopologyTier { - pub fn new(count: u32, source: DataSource) -> Self { - Self { count, source } - } -} - -impl Default for TopologyTier { - fn default() -> Self { - Self { - count: 1, - source: DataSource::default(), - } - } -} - -#[derive(Debug, Copy, Clone)] -pub struct TopologyCount { - pub sockets: TopologyTier, - pub cores: u32, - pub threads: u32, - pub source: DataSource, -} - -impl Default for TopologyCount { - fn default() -> Self { - TopologyCount { - sockets: TopologyTier::default(), - cores: 1, - threads: 1, - source: DataSource::DefaultValue, - } - } -} - -/// Where did this cpu information come from? -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub enum DataSource { - /// A default value , when lookup fails - #[default] - DefaultValue, - /// Value from Android getprop shell tool - AndroidGetprop, - /// Value generated from other inputs - Calculated(&'static str), - /// x86 cpuid instruction - Cpuid, - /// x86 cpuid instruction dump - CpuidDump, - /// Magic values from the cpu that need to be mapped to a readable value - CpuLookupTable, - /// model-specific registers (MSR) - CpuMsr, - /// value in cpu register on cpu reset - CpuReset, - /// from device tree - DeviceTree, - /// sysinfo command on Haiku - HaikuSysinfo, - /// /proc/cpuinfo - LinuxProcCpuinfo, - /// Linux virtual /sys directory tree - LinuxSysFs, - /// Determined from a set of pre-defined values - LookupTable, - /// Linux lscpu command - Lscpu, - /// x86 MpTable - MpTable, - /// value from sysctrl tool - Sysctrl(&'static str), - /// value from system call - SystemCall, - /// value from Windows registry - WindowsRegistry, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_ucfirst_empty() { - assert_eq!(ucfirst(""), ""); - } - - #[test] - fn test_ucfirst_already_upper() { - assert_eq!(ucfirst("Hello"), "Hello"); - } - - #[test] - fn test_ucfirst_lowercase() { - assert_eq!(ucfirst("hello"), "Hello"); - } - - #[test] - fn test_ucfirst_single_char() { - assert_eq!(ucfirst("a"), "A"); - } - - #[test] - fn test_cleanup_soc_vendor_brcm() { - assert_eq!(cleanup_soc_vendor("brcm"), "Broadcom"); - } - - #[test] - fn test_cleanup_soc_vendor_qcom() { - assert_eq!(cleanup_soc_vendor("qcom"), "Qualcomm"); - } - - #[test] - fn test_cleanup_soc_vendor_rk() { - assert_eq!(cleanup_soc_vendor("rk"), "Rockchip"); - } - - #[test] - fn test_cleanup_soc_vendor_allwinner() { - assert_eq!(cleanup_soc_vendor("sunxi"), "Allwinner"); - } - - #[test] - fn test_cleanup_soc_vendor_raspberrypi() { - assert_eq!(cleanup_soc_vendor("raspberrypi"), "Raspberry Pi"); - } - - #[test] - fn test_cleanup_soc_vendor_bigtreetech() { - assert_eq!(cleanup_soc_vendor("bigtreetech"), "BigTreeTech"); - } - - #[test] - fn test_cleanup_soc_vendor_other() { - assert_eq!(cleanup_soc_vendor("unknown_vendor"), "Unknown_vendor"); - } - - #[test] - fn test_core_type_from_str_performance() { - assert_eq!(CoreType::from("Performance"), CoreType::Performance); - } - - #[test] - fn test_core_type_from_str_efficiency() { - assert_eq!(CoreType::from("Efficiency"), CoreType::Efficiency); - } - - #[test] - fn test_core_type_from_str_super() { - assert_eq!(CoreType::from("Super"), CoreType::Super); - } - - #[test] - fn test_core_type_from_str_unknown_defaults_to_performance() { - assert_eq!(CoreType::from("Unknown"), CoreType::Performance); - } - - #[test] - fn test_core_type_from_string() { - let s = String::from("Efficiency"); - assert_eq!(CoreType::from(s), CoreType::Efficiency); - } - - #[test] - fn test_core_type_into_str() { - let s: &str = CoreType::Super.into(); - assert_eq!(s, "Super"); - let s: &str = CoreType::Performance.into(); - assert_eq!(s, "Performance"); - let s: &str = CoreType::Efficiency.into(); - assert_eq!(s, "Efficiency"); - } - - #[test] - fn test_topology_tier_new() { - let t = TopologyTier::new(4, DataSource::Cpuid); - assert_eq!(t.count, 4); - assert_eq!(t.source, DataSource::Cpuid); - } - - #[test] - fn test_topology_tier_default() { - let t = TopologyTier::default(); - assert_eq!(t.count, 1); - assert_eq!(t.source, DataSource::DefaultValue); - } - - #[test] - fn test_speed_default() { - let s = Speed::default(); - assert_eq!(s.base, 0); - assert_eq!(s.boost, 0); - assert!(!s.measured); - } - - #[test] - fn test_speed_values() { - let s = Speed { - base: 2400, - boost: 5000, - measured: false, - }; - assert_eq!(s.base, 2400); - assert_eq!(s.boost, 5000); - assert!(!s.measured); - } - - #[test] - fn test_speed_measured() { - let s = Speed { - base: 3000, - boost: 3000, - measured: true, - }; - assert!(s.measured); - } - - #[test] - fn test_cli_flags_default() { - let f = CliFlags::default(); - assert!(!f.color); - assert!(!f.verbose); - } - - #[test] - fn test_cli_flags_explicit() { - let f = CliFlags { - color: true, - compact: true, - verbose: true, - }; - assert!(f.color); - assert!(f.compact); - assert!(f.verbose); - } -} +pub use topology::*; +pub use util::*; diff --git a/src/common/os/linux_sysfs.rs b/src/common/os/linux_sysfs.rs index 8f663e9a..f2644e2b 100644 --- a/src/common/os/linux_sysfs.rs +++ b/src/common/os/linux_sysfs.rs @@ -1,14 +1,14 @@ #![cfg(linux_os)] use crate::common::{ - Cache, CacheLevel, CacheType, DataSource, Level1Cache, TopologyCount, expand_cpu_list, - get_proc_cpuinfo_data, parse_cpu_list_count, + Cache, CacheLevel, CacheType, DataSource, Level1Cache, TopologyCount, TopologyTier, + expand_cpu_list, get_proc_cpuinfo_data, parse_cpu_list_count, }; use std::collections::{BTreeMap, HashSet}; use std::fs; use std::path::Path; -/// Detects topology counts (threads and cores) from `/sys/devices/system/cpu`. +/// Detects topology counts (sockets, threads and cores) from `/sys/devices/system/cpu`. pub fn detect_sysfs_topology() -> TopologyCount { let mut topo = TopologyCount::default(); @@ -20,18 +20,24 @@ pub fn detect_sysfs_topology() -> TopologyCount { let cpus = expand_cpu_list(&online); let mut core_ids = HashSet::new(); + let mut package_ids = HashSet::new(); for cpu_id in cpus { - let core_id_path = cpu_root - .join(format!("cpu{}", cpu_id)) - .join("topology") - .join("core_id"); + let topo_dir = cpu_root.join(format!("cpu{}", cpu_id)).join("topology"); + let core_id_path = topo_dir.join("core_id"); if let Ok(id_str) = fs::read_to_string(&core_id_path) { core_ids.insert(id_str.trim().to_string()); } + let pkg_path = topo_dir.join("physical_package_id"); + if let Ok(id_str) = fs::read_to_string(&pkg_path) { + package_ids.insert(id_str.trim().to_string()); + } } if !core_ids.is_empty() { topo.cores = core_ids.len() as u32; } + if !package_ids.is_empty() { + topo.sockets = TopologyTier::new(package_ids.len() as u32, DataSource::LinuxSysFs); + } } if topo.threads == 0 { @@ -54,6 +60,22 @@ pub fn detect_sysfs_topology() -> TopologyCount { topo.cores = topo.threads; } + if topo.sockets.count == 0 { + let cpuinfo = get_proc_cpuinfo_data(); + let mut physical_ids = HashSet::new(); + for cpu_map in &cpuinfo { + if let Some(id) = cpu_map.get("physical id") { + physical_ids.insert(id.trim().to_string()); + } + } + if !physical_ids.is_empty() { + topo.sockets = + TopologyTier::new(physical_ids.len() as u32, DataSource::LinuxProcCpuinfo); + } else { + topo.sockets = TopologyTier::new(1, DataSource::DefaultValue); + } + } + topo } diff --git a/src/common/topology.rs b/src/common/topology.rs new file mode 100644 index 00000000..0d743df3 --- /dev/null +++ b/src/common/topology.rs @@ -0,0 +1,171 @@ +use crate::common::cache::Cache; + +/// CPU speed information (base and boost frequencies). +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct Speed { + /// Base frequency in MHz + pub base: u32, + /// Boost frequency in MHz + pub boost: u32, + /// Whether the frequency was measured (vs reported by CPU) + pub measured: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct TopologyTier { + pub count: u32, + pub source: DataSource, +} + +impl TopologyTier { + pub fn new(count: u32, source: DataSource) -> Self { + Self { count, source } + } +} + +impl Default for TopologyTier { + fn default() -> Self { + Self { + count: 1, + source: DataSource::default(), + } + } +} + +/// Complete CPU topology information including sockets, dies, cores, threads, speed, and cache. +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Topology { + /// Number of processor sockets + pub sockets: TopologyTier, + /// Number of dies per socket + pub dies: TopologyTier, + /// Number of physical cores + pub cores: TopologyTier, + /// Number of logical threads (includes SMT) + pub threads: TopologyTier, + /// CPU speed information + pub speed: Speed, + /// Cache hierarchy information + pub cache: Option, +} + +impl Topology { + pub fn new(sockets: u32, cores: u32, threads: u32) -> Self { + Self { + sockets: TopologyTier::new(sockets, DataSource::default()), + cores: TopologyTier::new(cores, DataSource::default()), + threads: TopologyTier::new(threads, DataSource::default()), + ..Default::default() + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TopologyCount { + pub sockets: TopologyTier, + pub cores: u32, + pub threads: u32, + pub source: DataSource, +} + +impl Default for TopologyCount { + fn default() -> Self { + TopologyCount { + sockets: TopologyTier::default(), + cores: 1, + threads: 1, + source: DataSource::DefaultValue, + } + } +} + +/// Where did this cpu information come from? +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub enum DataSource { + /// A default value , when lookup fails + #[default] + DefaultValue, + /// Value from Android getprop shell tool + AndroidGetprop, + /// Value generated from other inputs + Calculated(&'static str), + /// x86 cpuid instruction + Cpuid, + /// x86 cpuid instruction dump + CpuidDump, + /// Magic values from the cpu that need to be mapped to a readable value + CpuLookupTable, + /// model-specific registers (MSR) + CpuMsr, + /// value in cpu register on cpu reset + CpuReset, + /// from device tree + DeviceTree, + /// sysinfo command on Haiku + HaikuSysinfo, + /// /proc/cpuinfo + LinuxProcCpuinfo, + /// Linux virtual /sys directory tree + LinuxSysFs, + /// Determined from a set of pre-defined values + LookupTable, + /// Linux lscpu command + Lscpu, + /// x86 MpTable + MpTable, + /// value from sysctrl tool + Sysctrl(&'static str), + /// value from system call + SystemCall, + /// value from Windows registry + WindowsRegistry, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_topology_tier_new() { + let t = TopologyTier::new(4, DataSource::Cpuid); + assert_eq!(t.count, 4); + assert_eq!(t.source, DataSource::Cpuid); + } + + #[test] + fn test_topology_tier_default() { + let t = TopologyTier::default(); + assert_eq!(t.count, 1); + assert_eq!(t.source, DataSource::DefaultValue); + } + + #[test] + fn test_speed_default() { + let s = Speed::default(); + assert_eq!(s.base, 0); + assert_eq!(s.boost, 0); + assert!(!s.measured); + } + + #[test] + fn test_speed_values() { + let s = Speed { + base: 2400, + boost: 5000, + measured: false, + }; + assert_eq!(s.base, 2400); + assert_eq!(s.boost, 5000); + assert!(!s.measured); + } + + #[test] + fn test_speed_measured() { + let s = Speed { + base: 3000, + boost: 3000, + measured: true, + }; + assert!(s.measured); + } +} diff --git a/src/common/util.rs b/src/common/util.rs new file mode 100644 index 00000000..cdc7b88f --- /dev/null +++ b/src/common/util.rs @@ -0,0 +1,95 @@ +use alloc::string::String; + +pub fn ucfirst(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } +} + +pub fn cleanup_soc_vendor(s: &str) -> String { + let lower = s.to_lowercase(); + let vendor = match lower.as_str() { + "allwinner" | "sunxi" => "Allwinner", + "amlogic" | "meson" => "Amlogic", + "apple" => "Apple", + "bigtreetech" => "BigTreeTech", + "brcm" | "broadcom" => "Broadcom", + "hisilicon" | "hi" => "HiSilicon", + "mediatek" | "mtk" => "MediaTek", + "nxp" | "freescale" => "NXP", + "qcom" | "qualcomm" => "Qualcomm", + "raspberrypi" => "Raspberry Pi", + "realtek" => "Realtek", + "renesas" => "Renesas", + "rk" | "rockchip" => "Rockchip", + "samsung" | "exynos" => "Samsung", + "st" | "stmicro" => "STMicroelectronics", + "ti" => "Texas Instruments", + "xilinx" => "Xilinx", + _ => return ucfirst(s), + }; + + String::from(vendor) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ucfirst_empty() { + assert_eq!(ucfirst(""), ""); + } + + #[test] + fn test_ucfirst_already_upper() { + assert_eq!(ucfirst("Hello"), "Hello"); + } + + #[test] + fn test_ucfirst_lowercase() { + assert_eq!(ucfirst("hello"), "Hello"); + } + + #[test] + fn test_ucfirst_single_char() { + assert_eq!(ucfirst("a"), "A"); + } + + #[test] + fn test_cleanup_soc_vendor_brcm() { + assert_eq!(cleanup_soc_vendor("brcm"), "Broadcom"); + } + + #[test] + fn test_cleanup_soc_vendor_qcom() { + assert_eq!(cleanup_soc_vendor("qcom"), "Qualcomm"); + } + + #[test] + fn test_cleanup_soc_vendor_rk() { + assert_eq!(cleanup_soc_vendor("rk"), "Rockchip"); + } + + #[test] + fn test_cleanup_soc_vendor_allwinner() { + assert_eq!(cleanup_soc_vendor("sunxi"), "Allwinner"); + } + + #[test] + fn test_cleanup_soc_vendor_raspberrypi() { + assert_eq!(cleanup_soc_vendor("raspberrypi"), "Raspberry Pi"); + } + + #[test] + fn test_cleanup_soc_vendor_bigtreetech() { + assert_eq!(cleanup_soc_vendor("bigtreetech"), "BigTreeTech"); + } + + #[test] + fn test_cleanup_soc_vendor_other() { + assert_eq!(cleanup_soc_vendor("unknown_vendor"), "Unknown_vendor"); + } +} diff --git a/src/ppc/cpu.rs b/src/ppc/cpu.rs index e75c6b33..0bacfc41 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -4,7 +4,7 @@ use crate::common::cache::Cache; #[cfg(target_os = "linux")] use crate::common::get_proc_cpuinfo_data; use crate::common::os::TOSData; -use crate::common::{CoreType, DataSource, Speed, TDetect, UNK}; +use crate::common::{CoreType, DataSource, Speed, TDetect, Topology, TopologyTier, UNK}; use crate::ppc::micro_arch::{CpuArch, CpuCore, MicroArch}; use std::fs; use std::path::Path; @@ -22,9 +22,10 @@ pub struct PpcData { pub type Cpu = crate::common::Cpu; impl Cpu { - fn detect_topology() -> (u32, u32) { + fn detect_topology() -> (u32, u32, u32) { #[cfg(target_os = "linux")] { + let sysfs_topo = crate::common::detect_sysfs_topology(); let cpuinfo = get_proc_cpuinfo_data(); let proc_count = cpuinfo .iter() @@ -33,9 +34,16 @@ impl Cpu { let thread_count = if proc_count > 0 { proc_count + } else if sysfs_topo.threads > 0 { + sysfs_topo.threads } else { - let topo = crate::common::detect_sysfs_topology(); - if topo.threads > 0 { topo.threads } else { 1 } + 1 + }; + + let sockets = if sysfs_topo.sockets.count > 0 { + sysfs_topo.sockets.count + } else { + 1 }; // Check sysfs for SMT thread siblings per core @@ -43,14 +51,14 @@ impl Cpu { if let Ok(content) = fs::read_to_string(path) { let threads_per_core = crate::common::expand_cpu_list(&content).len().max(1) as u32; let core_count = (thread_count / threads_per_core).max(1); - return (core_count, thread_count); + return (sockets, core_count, thread_count); } - (thread_count, thread_count) + (sockets, thread_count, thread_count) } #[cfg(not(target_os = "linux"))] { - (1, 1) + (1, 1, 1) } } @@ -183,10 +191,10 @@ impl TDetect for Cpu { let version = (pvr >> 16) as u16; let revision = (pvr & 0xFFFF) as u16; let cpu_arch = CpuArch::find(pvr); - let (core_count, thread_count) = Self::detect_topology(); + let (socket_count, core_count, thread_count) = Self::detect_topology(); let mut cache = Self::detect_cache(); if let Some(c) = &mut cache { - c.resolve_share_counts(core_count, thread_count, 1); + c.resolve_share_counts(core_count, thread_count, socket_count); } let (clock_speed, clock_speed_source) = Self::detect_clock_speed(); let speed = clock_speed.map(|mhz| Speed { @@ -224,10 +232,20 @@ impl TDetect for Cpu { UNK }); + let topology = Topology { + sockets: TopologyTier::new(socket_count, DataSource::LinuxProcCpuinfo), + cores: TopologyTier::new(core_count, DataSource::LinuxProcCpuinfo), + threads: TopologyTier::new(thread_count, DataSource::LinuxProcCpuinfo), + speed: speed.unwrap_or_default(), + cache, + ..Default::default() + }; + Self { system, vendor, model: extra.cpu_arch.marketing_name.to_string(), + topology, cores, features: std::collections::BTreeMap::new(), extra, diff --git a/src/ppc/display.rs b/src/ppc/display.rs index 5bc73f72..1e42b3a9 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -23,17 +23,22 @@ impl TCpuDisplay for Cpu { let total_cores = self.total_cores(); let total_threads = self.total_threads(); - disp.display_topology_line( - total_cores, - total_threads, - self.is_hybrid(), - self.cores.len(), - ); + let sockets = self.total_sockets(); + + if total_cores > 1 || total_threads > 1 || sockets > 1 || flags.verbose { + disp.display_topology_line( + sockets, + total_cores, + total_threads, + self.is_hybrid(), + self.cores.len(), + ); + } if let Some(core) = self.cores.first() { disp.display_frequency(core.speed, flags); - disp.display_core_cache(core.cache, total_cores, 0); + disp.display_core_cache(core.cache, total_cores, sockets); } println!(); diff --git a/src/ppc/micro_arch.rs b/src/ppc/micro_arch.rs index 0fd488ae..3f93fb27 100644 --- a/src/ppc/micro_arch.rs +++ b/src/ppc/micro_arch.rs @@ -217,7 +217,9 @@ impl CpuArch { #[cfg(test)] mod tests { + use super::CpuCore; use super::*; + use crate::common::Topology; #[test] fn test_ppc_find_classic() { @@ -333,6 +335,7 @@ mod tests { system: None, vendor: String::from("IBM"), model: String::from("PowerPC 970"), + topology: Topology::new(1, 2, 2), cores: vec![core], features: std::collections::BTreeMap::new(), extra: PpcData { diff --git a/src/riscv/cpu.rs b/src/riscv/cpu.rs index 0c0f2ac4..679f337b 100644 --- a/src/riscv/cpu.rs +++ b/src/riscv/cpu.rs @@ -28,10 +28,21 @@ impl TDetect for Cpu { features_source: info.features_source, }; + let sockets = OS::get_socket_count(); + let total_cores = info.cores.iter().map(|c| c.count).sum(); + let total_threads = info.cores.iter().map(|c| c.threads).sum(); + let topology = Topology { + sockets, + cores: TopologyTier::new(total_cores, sockets.source), + threads: TopologyTier::new(total_threads, sockets.source), + ..Default::default() + }; + Self { system: OS::get_system_name(), vendor: info.vendor, model: info.model, + topology, cores: info.cores, features, extra, diff --git a/src/riscv/display.rs b/src/riscv/display.rs index b1b03653..2768fe07 100644 --- a/src/riscv/display.rs +++ b/src/riscv/display.rs @@ -34,9 +34,12 @@ impl CpuDisplay { disp.simple_line_opt("Process Node", cpu_info.cpu_arch.technology); + let sockets = cpu_info.total_sockets(); + // Display topology & per-core details if cpu_info.is_hybrid() { disp.display_topology_line( + sockets, cpu_info.total_cores(), cpu_info.total_threads(), true, @@ -61,16 +64,16 @@ impl CpuDisplay { }, ); - disp.display_core_cache(core.cache, core.count, 0); + disp.display_core_cache(core.cache, core.count, sockets); if core.cache.is_none() { disp.newline(); } } } else if let Some(core) = cpu_info.cores.first() { - disp.display_topology_line(core.count, core.threads, false, 1); + disp.display_topology_line(sockets, core.count, core.threads, false, 1); - disp.display_core_cache(core.cache, core.count, 0); + disp.display_core_cache(core.cache, core.count, sockets); disp.display_frequency(core.speed, flags); } diff --git a/src/x86/cpu.rs b/src/x86/cpu.rs index 88efd792..c274125a 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -2,7 +2,7 @@ use super::brand::CpuBrand; use super::micro_arch::{CpuArch, MicroArch}; -use super::topology::Topology; +use super::topology::{DomainList, Topology}; use super::vendor::Cyrix; use super::*; #[cfg(std_os)] @@ -241,8 +241,8 @@ pub struct X86Data { pub brand_id: u32, /// CPU signature (family, model, stepping) pub signature: CpuSignature, - /// Speed, threads, cores, sockets - pub topology: Topology, + /// Topology domains discovered from CPUID leaves + pub topology_domains: DomainList, } pub type CpuCore = crate::common::CpuCore; @@ -556,6 +556,7 @@ impl Cpu { let sig = CpuSignature::detect(); let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); let topology = Topology::detect_cpuid(); + let topology_domains = Topology::detect_domains(); let cores = Self::detect_cpuid_core_types(&arch, &topology); let extra = X86Data { @@ -569,13 +570,14 @@ impl Cpu { easter_egg: Self::easter_egg(), brand_id: get_brand_id(), signature: sig, - topology, + topology_domains, }; Self { system: None, vendor: String::from(extra.arch.brand_name), model: extra.arch.model.clone(), + topology, cores, features: get_feature_list(), extra, @@ -767,7 +769,6 @@ mod tests { brand_id: 0, easter_egg: None, signature: dummy_sig, - topology: Topology::default(), ..Default::default() }, features: get_feature_list(), @@ -782,7 +783,6 @@ mod tests { brand_id: 0, easter_egg: None, signature: dummy_sig, - topology: Topology::default(), ..Default::default() }, features: get_feature_list(), @@ -806,7 +806,6 @@ mod tests { brand_id: 0, easter_egg: None, signature: dummy_sig, - topology: Topology::default(), ..Default::default() }, features: get_feature_list(), @@ -824,7 +823,6 @@ mod tests { brand_id: 0, easter_egg: None, signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), - topology: Topology::default(), ..Default::default() }, features: get_feature_list(), @@ -846,7 +844,6 @@ mod tests { brand_id: 0, easter_egg: None, signature: CpuSignature::new(0, 6, 0, 0, 0, DataSource::DefaultValue), - topology: Topology::default(), ..Default::default() }, features: get_feature_list(), diff --git a/src/x86/display.rs b/src/x86/display.rs index 923bf285..b9545147 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -80,16 +80,12 @@ impl Cpu { fn print_topology(&self, flags: CliFlags, disp: &CpuDisplay) { if self.is_hybrid() { - disp.simple_line( - "Topology", - &format!( - "{} across {} core types", - CpuDisplay::format_core_threads( - self.topology.cores.count, - self.topology.threads.count - ), - self.cores.len() - ), + disp.display_topology_line( + self.topology.sockets.count, + self.topology.cores.count, + self.topology.threads.count, + true, + self.cores.len(), ); for (i, core) in self.cores.iter().enumerate() { @@ -131,29 +127,13 @@ impl Cpu { || self.topology.sockets.count > 1; if multi_core || flags.verbose { - let socket_str = CpuDisplay::plural(self.topology.sockets.count, "socket", "sockets"); - let core_str = CpuDisplay::plural(self.topology.cores.count, "core", "cores"); - let thread_str = CpuDisplay::plural(self.topology.threads.count, "thread", "threads"); - - if self.topology.sockets.count > 1 || flags.verbose { - disp.simple_line( - "Topology", - &format!( - "{} {socket_str}, {} {core_str}, {} {thread_str}", - self.topology.sockets.count, - self.topology.cores.count, - self.topology.threads.count, - ), - ); - } else { - disp.simple_line( - "Topology", - &CpuDisplay::format_core_threads( - self.topology.cores.count, - self.topology.threads.count, - ), - ); - } + disp.display_topology_line( + self.topology.sockets.count, + self.topology.cores.count, + self.topology.threads.count, + false, + 1, + ); } } diff --git a/src/x86/dos/mod.rs b/src/x86/dos/mod.rs index eb41293c..f7254673 100644 --- a/src/x86/dos/mod.rs +++ b/src/x86/dos/mod.rs @@ -32,27 +32,27 @@ pub fn enrich_cpu(cpu: &mut Cpu) { let total_cores = mp_table.total_cores(); let total_threads = mp_table.total_threads(); - if mp_sockets > 1 || total_threads > cpu.extra.topology.threads.count { + if mp_sockets > 1 || total_threads > cpu.topology.threads.count { let sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); - cpu.extra.topology.sockets = sockets; - let cores = cpu.extra.topology.cores.count.max(total_cores); - let threads = cpu.extra.topology.threads.count.max(total_threads); - cpu.extra.topology.cores = + cpu.topology.sockets = sockets; + let cores = cpu.topology.cores.count.max(total_cores); + let threads = cpu.topology.threads.count.max(total_threads); + cpu.topology.cores = TopologyTier::new(cores, DataSource::Calculated("MP Table * CPUID cores")); - cpu.extra.topology.threads = TopologyTier::new( + cpu.topology.threads = TopologyTier::new( threads, DataSource::Calculated("MP Table logical processors"), ); - if let Some(ref mut cache) = cpu.extra.topology.cache { + if let Some(ref mut cache) = cpu.topology.cache { cache.resolve_share_counts(cores, threads, mp_sockets); } } // 2. Calibrated PIT/TSC speed measurement fallback - if cpu.extra.topology.speed.base == 0 { + if cpu.topology.speed.base == 0 { let s = Speed::detect(); if s.base > 0 { - cpu.extra.topology.speed = s; + cpu.topology.speed = s; if !cpu.cores.is_empty() && cpu.cores[0].speed.is_none() { cpu.cores[0].speed = Some(s); } diff --git a/src/x86/efi/mod.rs b/src/x86/efi/mod.rs index fb625937..563becd0 100644 --- a/src/x86/efi/mod.rs +++ b/src/x86/efi/mod.rs @@ -34,36 +34,34 @@ pub fn enrich_cpu(cpu: &mut Cpu) { // 2. Multi-socket / multi-package topology from EFI MP Services / SMBIOS let efi_sockets = crate::x86::count::get_platform_socket_count(); if efi_sockets.count > 1 { - cpu.extra.topology.sockets = efi_sockets; + cpu.topology.sockets = efi_sockets; let cores = cpu - .extra .topology .cores .count .max(cpuid_cores_per_package() * efi_sockets.count); let threads = cpu - .extra .topology .threads .count .max(cpuid_threads_per_package() * efi_sockets.count); - cpu.extra.topology.cores = + cpu.topology.cores = TopologyTier::new(cores, DataSource::Calculated("EFI sockets * CPUID cores")); - cpu.extra.topology.threads = TopologyTier::new( + cpu.topology.threads = TopologyTier::new( threads, DataSource::Calculated("EFI sockets * CPUID threads"), ); - let sockets = cpu.extra.topology.sockets.count; - if let Some(ref mut cache) = cpu.extra.topology.cache { + let sockets = cpu.topology.sockets.count; + if let Some(ref mut cache) = cpu.topology.cache { cache.resolve_share_counts(cores, threads, sockets); } } // 3. Frequency measurement (TSC stall or SMBIOS fallback) - if cpu.extra.topology.speed.base == 0 { + if cpu.topology.speed.base == 0 { let measured = Speed::detect(); if measured.base > 0 { - cpu.extra.topology.speed = measured; + cpu.topology.speed = measured; if cpu.cores.len() == 1 && cpu.cores[0].speed.is_none() { cpu.cores[0].speed = Some(measured); } diff --git a/src/x86/topology.rs b/src/x86/topology.rs index c9f49901..b1fb0ec8 100644 --- a/src/x86/topology.rs +++ b/src/x86/topology.rs @@ -1,5 +1,6 @@ use super::constants::*; use super::{cpuid_data_source, is_valid_leaf, vendor_str, x86_cpuid_count}; +pub use crate::common::Topology; use crate::common::{Cache, DataSource, Speed, TopologyTier}; use crate::x86::{cpuid_cores_per_package, cpuid_threads_per_package}; use alloc::vec::Vec; @@ -161,9 +162,9 @@ impl Speed { /// Represents a topology domain (thread, core, die, socket, etc.). #[derive(Debug, Default, Copy, Clone, PartialEq)] pub struct TopologyDomain { - level: u32, - kind: TopologyType, - count: u32, + pub level: u32, + pub kind: TopologyType, + pub count: u32, } /// CPU topology domain type. @@ -190,26 +191,6 @@ pub enum TopologyType { pub type DomainList = Vec; -/// Complete CPU topology information including sockets, cores, threads, and cache. -#[derive(Debug, Default, PartialEq)] -pub struct Topology { - /// Number of processor sockets - pub sockets: TopologyTier, - /// Number of dies per socket - pub dies: TopologyTier, - /// Number of physical cores - pub cores: TopologyTier, - /// Number of logical threads (includes SMT) - pub threads: TopologyTier, - /// CPU speed information - pub speed: Speed, - /// Cache hierarchy information - pub cache: Option, - - #[allow(unused)] - domains: DomainList, -} - impl Topology { /// Detects CPU topology purely from CPUID leaves without touching OS information. #[must_use] @@ -251,7 +232,6 @@ impl Topology { threads, speed, cache, - domains, } } @@ -400,7 +380,7 @@ impl Topology { ) } - fn detect_domains() -> DomainList { + pub(crate) fn detect_domains() -> DomainList { let d: DomainList = Vec::new(); if !is_valid_leaf(LEAF_0B) { From a2ceaf70828f1e741e55562650eb283df3d326dc Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 15:45:30 -0400 Subject: [PATCH 23/30] Update changelog --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b6a0e8..470ad9dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,34 @@ # Changelog -## [2.1.0] +## [2.1.0] — Unified topology model, multi-socket display, PowerPC fixes, and common deduplication + +### Added +- **Unified CPU Topology on Common Struct**: Extracted the `Topology` struct (`sockets`, `dies`, `cores`, `threads`, `speed`, `cache`) into `src/common/topology.rs` and placed `topology: Topology` directly on the shared `Cpu` object across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`) +- **Multi-Socket Display Support**: Added physical socket count formatting across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`); outputs `Topology: sockets, cores, threads` whenever physical socket count is greater than 1 or in verbose mode (`src/common/display.rs`) +- **Sysfs Socket Detection**: Added physical package ID parsing from `/sys/devices/system/cpu/cpu*/topology/physical_package_id` and `/proc/cpuinfo` `physical id` to automatically detect multi-socket systems on Linux/Android (`src/common/os/linux_sysfs.rs`) +- **Single-Core SMT Display**: `display_topology_line` now explicitly displays thread count for single-core hyperthreaded CPUs (e.g. `1 core (2 threads)`) (`src/common/display.rs`) +- **Centralized Linux Sysfs Module**: Extracted sysfs cache tree traversal (`read_sysfs_cpu_cache`, `read_sysfs_cache_per_type`) and topology reader (`detect_sysfs_topology`) into a shared `linux_sysfs` module compiled under `#[cfg(linux_os)]` (`src/common/os/linux_sysfs.rs`) +- **Cross-Compilation CI & Target Checks**: Added `check-all` recipe to `justfile` and `Makefile` and integrated target compilation checks in GitHub Actions CI covering `x86_64-unknown-uefi`, `powerpc-unknown-linux-gnu`, `riscv64gc-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, and `aarch64-pc-windows-msvc` (`.github/workflows/ci.yml`, `justfile`, `Makefile`) +- **Common Device-Tree and Integer Frequency Helpers**: Added pure `core`/`no_std` safe integer frequency parser `parse_frequency_mhz`, devicetree helpers (`read_devicetree_string`, `read_devicetree_u64`), and thread affinity iterator `for_each_logical_core` (`src/common/os/common.rs`) +- **Xeon Dual-Socket Example**: Added dual-socket Pentium Pro / Xeon test fixture output (`examples/2PPRO.TXT`) + +### Changed +- **Modularized Common Module**: Split `src/common/mod.rs` into single-responsibility submodules: + - `src/common/cpu.rs`: Abstract CPU definitions (`CoreType`, `CpuCore`, `Cpu`, `TDetect`) + - `src/common/topology.rs`: Topology and clock frequency (`Topology`, `TopologyTier`, `TopologyCount`, `Speed`, `DataSource`) + - `src/common/util.rs`: String and vendor normalization helpers (`ucfirst`, `cleanup_soc_vendor`) + - `src/common/display.rs`: Display formatter, CLI flags (`CliFlags`), and display traits (`TCpuDisplay`) + - Retained clean `pub use` re-exports in `src/common/mod.rs` for full backward compatibility +- **x86 Data Model Alignment**: Replaced `X86Data.topology` with `pub topology_domains: DomainList` (discovering raw CPUID leaf domains from Leaf 0B, 1F, and 80000026H), unifying outer `cpu.topology` access across all architectures (`src/x86/cpu.rs`, `src/x86/topology.rs`) +- **Deduplicated ARM Linux & Android OS Detection**: Replaced separate `src/arm/os/android.rs` (345 lines) with unified `src/arm/os/linux.rs` guarded by `#[cfg(linux_os)]` +- **Separated CPUID Discovery from OS Enrichment**: Clarified separation between pure CPUID detection and live OS hardware enrichment (`enrich_cpu`) across DOS, EFI, and OS targets (`src/x86/cpu.rs`, `src/x86/os/mod.rs`, `src/x86/efi/mod.rs`, `src/x86/dos/mod.rs`) +- **Architecture Module Alignment**: Refactored ARM, RISC-V, and PowerPC modules to follow consistent structure and display patterns (`src/arm/cpu.rs`, `src/riscv/cpu.rs`, `src/ppc/cpu.rs`) ### Fixed -- DOS Topology detection using Intel MPTables was assuming one APIC id = 1 socket, rather than one APIC id = 1 logical cpu. (This caused a Core 2 Quad to show 4 sockets, 16 cores, 16 threads) +- **PowerPC Clock Speed Detection**: Fixed clock speed reporting on Linux PowerPC (e.g. PowerBook G4 / PowerBook5,2) by prioritizing `/proc/cpuinfo` `clock: MHz`, sysfs `cpufreq`, and CPU node devicetree clock (`/proc/device-tree/cpus/*/clock-frequency`) over root bus frequency (`/proc/device-tree/clock-frequency`, which reports the 166.66 MHz FSB bus clock) (`src/ppc/cpu.rs`) +- **PowerPC Core & Thread Count**: Fixed spurious 2 cores / 2 threads detection on single-core PowerPC Macs by filtering `/proc/cpuinfo` blocks by `processor` key, ignoring trailing non-processor sections (`platform: PowerBook`) (`src/ppc/cpu.rs`) +- **DOS Multi-Socket MP Table Detection**: Corrected DOS Intel MP Table topology calculation: MP Table entries represent logical processors (APIC IDs) rather than physical sockets, resolving false multi-socket reports on multi-core processors (e.g. Core 2 Quad previously reporting 4 sockets, 16 cores, 16 threads) (`src/x86/dos/mod.rs`, `src/x86/dos/mp.rs`) +- **Cross-Platform Thread Affinity**: Unified logical core affinity switching loop in ARM Linux, Windows, and BSD to use `for_each_logical_core` ## [2.0.0] — Add missing Intel and AMD cpu mappings, fix edge cases, and more From 3cdf9516400c0f3cd24c7cffcb5efe74fa55d6e9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 15:54:17 -0400 Subject: [PATCH 24/30] Add Pentium 4 Northwood dump to verify detection/display of multiple threads --- src/x86/vendor/intel.rs | 11 ++++++++++- tests/cpuid/dump/P4Northwood.txt | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/cpuid/dump/P4Northwood.txt diff --git a/src/x86/vendor/intel.rs b/src/x86/vendor/intel.rs index 01419e2b..36f8bd4d 100644 --- a/src/x86/vendor/intel.rs +++ b/src/x86/vendor/intel.rs @@ -406,7 +406,16 @@ impl Intel { // NetBurst (P4 / Xeon) (0, 15, 0, 0, _) => brand_arch(MicroArch::Willamette, "Willamette", Some(N180)), (0, 15, 0, 1, _) => brand_arch(MicroArch::Willamette, "Willamette/Foster", Some(N180)), - (0, 15, 0, 2, _) => brand_arch(MicroArch::Northwood, "Northwood/Gallatin", Some(N130)), + (0, 15, 0, 2, _) => brand_arch( + MicroArch::Northwood, + if model.contains("Xeon") { + "Gallatin" + } else { + "Northwood" + }, + Some(N130), + ), + (0, 15, 0, 3, _) => brand_arch(MicroArch::Prescott, "Prescott", Some(N90)), (0, 15, 0, 4, _) => brand_arch(MicroArch::Prescott, "Prescott/Potomac", Some(N90)), (0, 15, 0, 6, _) => brand_arch(MicroArch::CedarMill, "Cedar Mill/Tulsa", Some(N64)), diff --git a/tests/cpuid/dump/P4Northwood.txt b/tests/cpuid/dump/P4Northwood.txt new file mode 100644 index 00000000..2917d139 --- /dev/null +++ b/tests/cpuid/dump/P4Northwood.txt @@ -0,0 +1,18 @@ +CPU 0: + 0x00000000 0x00: eax=0x00000002 ebx=0x756E6547 ecx=0x6C65746E edx=0x49656E69 + 0x00000001 0x00: eax=0x00000F27 ebx=0x00020809 ecx=0x00004400 edx=0xBFEBFBFF + 0x00000002 0x00: eax=0x665B5001 ebx=0x00000000 ecx=0x00000000 edx=0x007B7040 + 0x80000000 0x00: eax=0x80000004 ebx=0x00000000 ecx=0x00000000 edx=0x00000000 + 0x80000001 0x00: eax=0x00000000 ebx=0x00000000 ecx=0x00000000 edx=0x00000000 + 0x80000002 0x00: eax=0x20202020 ebx=0x20202020 ecx=0x20202020 edx=0x6E492020 + 0x80000003 0x00: eax=0x286C6574 ebx=0x50202952 ecx=0x69746E65 edx=0x52286D75 + 0x80000004 0x00: eax=0x20342029 ebx=0x20555043 ecx=0x36302E33 edx=0x007A4847 +CPU 1: + 0x00000000 0x00: eax=0x00000002 ebx=0x756E6547 ecx=0x6C65746E edx=0x49656E69 + 0x00000001 0x00: eax=0x00000F27 ebx=0x00020809 ecx=0x00004400 edx=0xBFEBFBFF + 0x00000002 0x00: eax=0x665B5001 ebx=0x00000000 ecx=0x00000000 edx=0x007B7040 + 0x80000000 0x00: eax=0x80000004 ebx=0x00000000 ecx=0x00000000 edx=0x00000000 + 0x80000001 0x00: eax=0x00000000 ebx=0x00000000 ecx=0x00000000 edx=0x00000000 + 0x80000002 0x00: eax=0x20202020 ebx=0x20202020 ecx=0x20202020 edx=0x6E492020 + 0x80000003 0x00: eax=0x286C6574 ebx=0x50202952 ecx=0x69746E65 edx=0x52286D75 + 0x80000004 0x00: eax=0x20342029 ebx=0x20555043 ecx=0x36302E33 edx=0x007A4847 From 143361f9b1a52282ba5a917485536e883e42afe6 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 16:59:43 -0400 Subject: [PATCH 25/30] Add color to dos version --- CHANGELOG.md | 9 +- src/common/display.rs | 17 +++ src/dos_rustid.rs | 11 +- src/x86/display.rs | 37 +++--- src/x86/dos/mod.rs | 256 +++++++++++++++++++++++++++++++++++++-- tools/elf2le/src/main.rs | 6 +- 6 files changed, 296 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 470ad9dd..33f0cd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [2.1.0] — Unified topology model, multi-socket display, PowerPC fixes, and common deduplication ### Added +- **DOS Colored Console Display**: Added driverless VGA text mode ANSI color rendering for 32-bit protected mode DOS (`dos32a` build, `rustid.exe`), matching output on CLI and UEFI targets; enabled `color: true` by default, added `/M` / `/MONO` flag support for monochrome output, and automatically stripped color escape sequences when output redirection to a file is detected (`src/dos_rustid.rs`, `src/x86/dos/mod.rs`) - **Unified CPU Topology on Common Struct**: Extracted the `Topology` struct (`sockets`, `dies`, `cores`, `threads`, `speed`, `cache`) into `src/common/topology.rs` and placed `topology: Topology` directly on the shared `Cpu` object across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`) - **Multi-Socket Display Support**: Added physical socket count formatting across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`); outputs `Topology: sockets, cores, threads` whenever physical socket count is greater than 1 or in verbose mode (`src/common/display.rs`) - **Sysfs Socket Detection**: Added physical package ID parsing from `/sys/devices/system/cpu/cpu*/topology/physical_package_id` and `/proc/cpuinfo` `physical id` to automatically detect multi-socket systems on Linux/Android (`src/common/os/linux_sysfs.rs`) @@ -13,20 +14,12 @@ - **Xeon Dual-Socket Example**: Added dual-socket Pentium Pro / Xeon test fixture output (`examples/2PPRO.TXT`) ### Changed -- **Modularized Common Module**: Split `src/common/mod.rs` into single-responsibility submodules: - - `src/common/cpu.rs`: Abstract CPU definitions (`CoreType`, `CpuCore`, `Cpu`, `TDetect`) - - `src/common/topology.rs`: Topology and clock frequency (`Topology`, `TopologyTier`, `TopologyCount`, `Speed`, `DataSource`) - - `src/common/util.rs`: String and vendor normalization helpers (`ucfirst`, `cleanup_soc_vendor`) - - `src/common/display.rs`: Display formatter, CLI flags (`CliFlags`), and display traits (`TCpuDisplay`) - - Retained clean `pub use` re-exports in `src/common/mod.rs` for full backward compatibility -- **x86 Data Model Alignment**: Replaced `X86Data.topology` with `pub topology_domains: DomainList` (discovering raw CPUID leaf domains from Leaf 0B, 1F, and 80000026H), unifying outer `cpu.topology` access across all architectures (`src/x86/cpu.rs`, `src/x86/topology.rs`) - **Deduplicated ARM Linux & Android OS Detection**: Replaced separate `src/arm/os/android.rs` (345 lines) with unified `src/arm/os/linux.rs` guarded by `#[cfg(linux_os)]` - **Separated CPUID Discovery from OS Enrichment**: Clarified separation between pure CPUID detection and live OS hardware enrichment (`enrich_cpu`) across DOS, EFI, and OS targets (`src/x86/cpu.rs`, `src/x86/os/mod.rs`, `src/x86/efi/mod.rs`, `src/x86/dos/mod.rs`) - **Architecture Module Alignment**: Refactored ARM, RISC-V, and PowerPC modules to follow consistent structure and display patterns (`src/arm/cpu.rs`, `src/riscv/cpu.rs`, `src/ppc/cpu.rs`) ### Fixed - **PowerPC Clock Speed Detection**: Fixed clock speed reporting on Linux PowerPC (e.g. PowerBook G4 / PowerBook5,2) by prioritizing `/proc/cpuinfo` `clock: MHz`, sysfs `cpufreq`, and CPU node devicetree clock (`/proc/device-tree/cpus/*/clock-frequency`) over root bus frequency (`/proc/device-tree/clock-frequency`, which reports the 166.66 MHz FSB bus clock) (`src/ppc/cpu.rs`) -- **PowerPC Core & Thread Count**: Fixed spurious 2 cores / 2 threads detection on single-core PowerPC Macs by filtering `/proc/cpuinfo` blocks by `processor` key, ignoring trailing non-processor sections (`platform: PowerBook`) (`src/ppc/cpu.rs`) - **DOS Multi-Socket MP Table Detection**: Corrected DOS Intel MP Table topology calculation: MP Table entries represent logical processors (APIC IDs) rather than physical sockets, resolving false multi-socket reports on multi-core processors (e.g. Core 2 Quad previously reporting 4 sockets, 16 cores, 16 threads) (`src/x86/dos/mod.rs`, `src/x86/dos/mp.rs`) - **Cross-Platform Thread Affinity**: Unified logical core affinity switching loop in ARM Linux, Windows, and BSD to use `for_each_logical_core` diff --git a/src/common/display.rs b/src/common/display.rs index a74a1bc1..8f5de58c 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -977,4 +977,21 @@ mod tests { assert!(f.color); assert!(f.verbose); } + + #[test] + fn test_display_features_with_vendor_key() { + use alloc::collections::BTreeMap; + + let disp = CpuDisplay { + flags: CliFlags::default(), + }; + let mut features = BTreeMap::new(); + features.insert("Base", String::from("FPU TSC MMX")); + features.insert("Centaur", String::from("RNG ACE PHE")); + + let keys = [ + "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", "Cyrix", + ]; + disp.display_features(&features, &keys); + } } diff --git a/src/dos_rustid.rs b/src/dos_rustid.rs index 4dc6c302..b0ef5bbf 100644 --- a/src/dos_rustid.rs +++ b/src/dos_rustid.rs @@ -36,6 +36,7 @@ fn help() { println!(" ?, H, HELP Show this help message"); println!(); println!("Flags (use / or - prefix):"); + println!(" /M, /MONO Don't output color"); println!(" /V, /VERBOSE Output more detailed information"); println!(); println!("Examples: RUSTID /E RUSTID /VERBOSE"); @@ -54,7 +55,10 @@ pub extern "C" fn rust_main() -> ! { let args = get_args(); - let mut flags = CliFlags::default(); + let mut flags = CliFlags { + color: true, + ..Default::default() + }; let mut action = "default"; let mut had_error = false; @@ -107,6 +111,10 @@ pub extern "C" fn rust_main() -> ! { flags.verbose = true; continue 'args; } + "MONO" => { + flags.color = false; + continue 'args; + } "DEBUG" => { action = "debug"; continue 'args; @@ -134,6 +142,7 @@ pub extern "C" fn rust_main() -> ! { for c in upper.chars() { match c { 'V' => flags.verbose = true, + 'M' => flags.color = false, 'D' => action = "debug", 'E' => action = "everything", 'R' => action = "dump", diff --git a/src/x86/display.rs b/src/x86/display.rs index b9545147..a80701ae 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -200,7 +200,7 @@ impl Cpu { } #[cfg(not(dos_real))] - fn print_centaur_features(&self, flags: CliFlags, disp: &CpuDisplay) { + fn format_centaur_features(&self, flags: CliFlags) -> Option { use alloc::vec::Vec; let centaur_map = vendor::Centaur::get_feature_list(); @@ -211,7 +211,7 @@ impl Cpu { list.push(String::from(*name)); } else { if flags.color { - list.push(CpuDisplay::ansi_color(ANSI_BRIGHT_BLACK, name)) + list.push(CpuDisplay::ansi_color(ANSI_BRIGHT_BLACK, name)); } else { list.push(format!("{name}(disabled)")); } @@ -219,24 +219,28 @@ impl Cpu { } if !list.is_empty() { - println!("{}{}", disp.sublabel("Centaur"), list.join(", ")); + return Some(list.join(", ")); } } + None } #[allow(unused_variables)] fn print_features(&self, flags: CliFlags, disp: &CpuDisplay) { - if !self.features.is_empty() { + let mut features = self.features.clone(); + + #[cfg(not(dos_real))] + if is_centaur() + && let Some(centaur_str) = self.format_centaur_features(flags) + { + features.insert("Centaur", centaur_str); + } + + if !features.is_empty() { let keys = [ - "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", + "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", "Cyrix", ]; - disp.display_features(&self.features, &keys); - - // Centaur features list - #[cfg(not(dos_real))] - if is_centaur() { - self.print_centaur_features(flags, disp); - } + disp.display_features(&features, &keys); } } } @@ -431,14 +435,17 @@ impl TCpuDisplay for Cpu { let cyrix = vendor::Cyrix::detect(); if cyrix.dir0 != 0xFF { - println!("{}Model number: {:X}h", disp.label("Cyrix"), cyrix.dir0); + println!( + "{}{:X}h", + disp.inline_sublabel("Cyrix", "Model number"), + cyrix.dir0 + ); println!("{}{:X}h", disp.sublabel("Revision"), cyrix.revision); println!("{}{:X}h", disp.sublabel("Stepping"), cyrix.stepping); if !cyrix.multiplier.is_empty() && cyrix.multiplier != "0" { println!("{}{}x", disp.sublabel("Bus Multiplier"), &cyrix.multiplier); } - #[cfg(not(dos_os))] - println!(); + disp.newline(); } } } diff --git a/src/x86/dos/mod.rs b/src/x86/dos/mod.rs index f7254673..419af050 100644 --- a/src/x86/dos/mod.rs +++ b/src/x86/dos/mod.rs @@ -66,22 +66,18 @@ pub fn enrich_cpu(cpu: &mut Cpu) { #[cold] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { - use crate::println; - #[cfg(dos_ext)] if let Some(location) = _info.location() { - println!( + crate::println!( "Panic in file '{}' at line {}:{}", location.file(), location.line(), location.column(), ); } else { - println!("Panic for unknown reason."); + crate::println!("Panic for unknown reason."); } - #[cfg(dos_real)] - println!("Panic!"); exit(1); } @@ -120,19 +116,253 @@ macro_rules! println { }; } -/// Writes a string to the DOS console. +#[cfg(dos_ext)] +static mut INITIAL_ATTR: u8 = 0x07; +#[cfg(dos_ext)] +static mut CURRENT_ATTR: u8 = 0x07; +#[cfg(dos_ext)] +static mut ATTR_INITIALIZED: bool = false; + +#[cfg(dos_ext)] +fn is_stdout_redirected() -> bool { + let dev_info: u16; + unsafe { + asm!( + "int 0x21", + in("ah") 0x44_u8, + in("al") 0x00_u8, + in("bx") 1_u16, // STDOUT handle + lateout("dx") dev_info, + lateout("ax") _, + options(preserves_flags) + ); + } + (dev_info & 0x80) == 0 +} + +#[cfg(dos_ext)] +fn get_cursor_pos() -> (u8, u8) { + let dx: u16; + unsafe { + asm!( + "int 0x10", + in("ah") 0x03_u8, + in("bh") 0_u8, + lateout("dx") dx, + lateout("ax") _, + lateout("cx") _, + options(preserves_flags) + ); + } + let row = (dx >> 8) as u8; + let col = (dx & 0xFF) as u8; + (row, col) +} + +#[cfg(dos_ext)] +fn set_cursor_pos(row: u8, col: u8) { + unsafe { + asm!( + "int 0x10", + in("ah") 0x02_u8, + in("bh") 0_u8, + in("dh") row, + in("dl") col, + lateout("ax") _, + options(preserves_flags) + ); + } +} + +#[cfg(dos_ext)] +fn dos_console_write(s: &str) { + let video_mode = peek_u8(0x00400049); + let cols = { + let c = peek_u16(0x0040004A) as usize; + if c == 0 { 80 } else { c } + }; + let rows = { + let r = peek_u8(0x00400084) as usize; + if r == 0 { 25 } else { r + 1 } + }; + let vram_base = if video_mode == 7 { + 0x000B0000 as *mut u16 + } else { + 0x000B8000 as *mut u16 + }; + + let (mut row, mut col) = { + let (r, c) = get_cursor_pos(); + (r as usize, c as usize) + }; + + if unsafe { !ATTR_INITIALIZED } { + let offset = row * cols + col; + let cell = unsafe { core::ptr::read_volatile(vram_base.add(offset)) }; + let existing_attr = (cell >> 8) as u8; + let init_a = if existing_attr != 0 { + existing_attr + } else { + 0x07 + }; + unsafe { + INITIAL_ATTR = init_a; + CURRENT_ATTR = init_a; + ATTR_INITIALIZED = true; + } + } + + let mut chars = s.chars().peekable(); + + while let Some(ch) = chars.next() { + // Parse ANSI escape sequence \x1b[...m + if ch == '\x1b' && chars.peek() == Some(&'[') { + chars.next(); // Consume '[' + let mut code = 0u32; + let mut has_code = false; + while let Some(&c) = chars.peek() { + if c.is_ascii_digit() { + code = code * 10 + (c as u32 - '0' as u32); + has_code = true; + chars.next(); + } else if c == ';' || c == 'm' { + chars.next(); + let fg = match code { + 0 => unsafe { INITIAL_ATTR & 0x0F }, + 30 => 0x00, // Black + 31 => 0x04, // Red + 32 | 92 => 0x0A, // Light Green + 33 | 93 => 0x0E, // Yellow + 34 => 0x09, // Light Blue + 35 | 95 => 0x0D, // Light Magenta + 36 | 96 => 0x0B, // Light Cyan + 37 => 0x07, // Light Gray + 90 => 0x08, // Dark Gray + 91 => 0x0C, // Light Red + 94 => 0x0B, // Light Cyan + 97 => 0x0F, // High-Intensity White + _ => unsafe { INITIAL_ATTR & 0x0F }, + }; + unsafe { + CURRENT_ATTR = (INITIAL_ATTR & 0xF0) | fg; + } + if c == 'm' { + break; + } + code = 0; + has_code = false; + } else { + break; + } + } + if !has_code && chars.peek() == Some(&'m') { + chars.next(); + unsafe { + CURRENT_ATTR = INITIAL_ATTR; + } + } + continue; + } + + match ch { + '\r' => col = 0, + '\n' => { + col = 0; + row += 1; + } + '\t' => { + col = (col + 8) & !7; + } + _ => { + let attr = unsafe { CURRENT_ATTR }; + let cell = ((attr as u16) << 8) | (ch as u16 & 0xFF); + let offset = row * cols + col; + unsafe { + core::ptr::write_volatile(vram_base.add(offset), cell); + } + col += 1; + } + } + + if col >= cols { + col = 0; + row += 1; + } + + if row >= rows { + let line_words = cols; + let total_words = (rows - 1) * cols; + unsafe { + core::ptr::copy(vram_base.add(line_words), vram_base, total_words); + let attr = CURRENT_ATTR; + let blank_cell = ((attr as u16) << 8) | (' ' as u16); + let last_line = vram_base.add(total_words); + for i in 0..cols { + core::ptr::write_volatile(last_line.add(i), blank_cell); + } + } + row = rows - 1; + } + } + + set_cursor_pos(row as u8, col as u8); +} + +#[cfg(dos_ext)] +fn write_redirected_str(s: &str) { + let mut chars = s.chars().peekable(); + let mut buf = [0u8; 256]; + let mut buf_len = 0; + + let flush_buf = |buf: &mut [u8; 256], buf_len: &mut usize| { + if *buf_len > 0 { + let mut offset = 0; + while offset < *buf_len { + let chunk_size = (*buf_len - offset).min(32767); + write_chunk(&buf[offset..offset + chunk_size]); + offset += chunk_size; + } + *buf_len = 0; + } + }; + + while let Some(ch) = chars.next() { + if ch == '\x1b' && chars.peek() == Some(&'[') { + chars.next(); // Consume '[' + while let Some(&c) = chars.peek() { + chars.next(); + if c == 'm' { + break; + } + } + continue; + } + + let mut code_units = [0u8; 4]; + let encoded = ch.encode_utf8(&mut code_units); + for &b in encoded.as_bytes() { + buf[buf_len] = b; + buf_len += 1; + if buf_len >= buf.len() { + flush_buf(&mut buf, &mut buf_len); + } + } + } + + flush_buf(&mut buf, &mut buf_len); +} + +/// Writes a string to the DOS console or redirected file. pub fn _print_str(s: &str) { #[cfg(dos_ext)] { if s.is_empty() { return; } - let bytes = s.as_bytes(); - let mut offset = 0; - while offset < bytes.len() { - let chunk_size = (bytes.len() - offset).min(32767); - write_chunk(&bytes[offset..offset + chunk_size]); - offset += chunk_size; + if is_stdout_redirected() { + write_redirected_str(s); + } else { + dos_console_write(s); } } #[cfg(dos_real)] diff --git a/tools/elf2le/src/main.rs b/tools/elf2le/src/main.rs index 815fcb02..1357d7df 100644 --- a/tools/elf2le/src/main.rs +++ b/tools/elf2le/src/main.rs @@ -103,12 +103,12 @@ fn main() -> Result<()> { text_va = s_va; text_size = s_size; text_foff = s_foff; - } else if name == ".rodata" { + } else if name == ".rodata" || name == ".data" || name.starts_with(".rodata.") || name.starts_with(".data.") { if data_va == 0 || s_va < data_va { data_va = s_va; } data_filesz += s_size; - } else if name == ".bss" { + } else if name == ".bss" || name.starts_with(".bss.") { if data_va == 0 || s_va < data_va { data_va = s_va; } @@ -132,7 +132,7 @@ fn main() -> Result<()> { for i in 0..e_shnum { let (s_name, _, s_va, s_foff, s_size) = sh_entry(i); let name = sh_name_str(s_name as usize); - if (name == ".rodata" || name == ".data") && s_size > 0 { + if (name == ".rodata" || name == ".data" || name.starts_with(".rodata.") || name.starts_with(".data.")) && s_size > 0 { let rel = (s_va - data_va) as usize; let src = &elf[s_foff as usize..(s_foff + s_size) as usize]; data_file_data[rel..rel + s_size as usize].copy_from_slice(src); From eff2c64beb5df4503a8704d56faecc188408ddfa Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 20:38:19 -0400 Subject: [PATCH 26/30] Fix android build and add the android check to the check-all recipe --- Makefile | 7 ++++++- justfile | 7 ++++++- src/common/os/android.rs | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 35119753..557677da 100644 --- a/Makefile +++ b/Makefile @@ -74,13 +74,18 @@ check-win-arm: @if ! rustup target list --installed | grep -q aarch64-pc-windows-msvc; then rustup target add aarch64-pc-windows-msvc; fi cargo check --target aarch64-pc-windows-msvc +# Compile check for Android ARM64 +check-android: + @if ! rustup target list --installed | grep -q aarch64-linux-android; then rustup target add aarch64-linux-android; fi + cargo check --target aarch64-linux-android + # Compile check for 32-bit Linux 486 check-486: @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi cargo +nightly check -Zjson-target-spec -Z build-std=std,core,alloc,panic_abort --target build-config/i486-linux.json --release # Compile check for all supported targets and platforms -check-all: check check-efi check-dos check-riscv check-win-arm check-486 +check-all: check check-efi check-dos check-riscv check-win-arm check-android check-486 # More in-depth code style checking lint: diff --git a/justfile b/justfile index 79326372..56613229 100644 --- a/justfile +++ b/justfile @@ -53,13 +53,18 @@ check-win-arm: @if ! rustup target list --installed | grep -q aarch64-pc-windows-msvc; then rustup target add aarch64-pc-windows-msvc; fi cargo check --target aarch64-pc-windows-msvc +# Compile check for Android ARM64 +check-android: + @if ! rustup target list --installed | grep -q aarch64-linux-android; then rustup target add aarch64-linux-android; fi + cargo check --target aarch64-linux-android + # Compile check for 32-bit Linux 486 check-486: @if ! rustup component list --installed --toolchain nightly | grep -q rust-src; then rustup component add rust-src --toolchain nightly; fi cargo +nightly check -Zjson-target-spec -Z build-std=std,core,alloc,panic_abort --target build-config/i486-linux.json --release # Compile check for all supported targets and platforms -check-all: check check-efi check-dos check-riscv check-win-arm check-486 +check-all: check check-efi check-dos check-riscv check-win-arm check-android check-486 # More in-depth code style checking lint: diff --git a/src/common/os/android.rs b/src/common/os/android.rs index 287731f3..089cbeef 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -2,8 +2,9 @@ use super::linux_sysfs::*; use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, cleanup_soc_vendor, - is_generic_value, + CacheLevel, CacheType, DataSource, Level1Cache, OS, TDetect, TOSData, TopologyCount, + TopologyTier, cleanup_soc_vendor, format_compatible_pair, get_devicetree_compatible, + get_proc_cpuinfo_data, is_generic_value, }; use std::collections::HashMap; From feaceb74d1b39b94e17c495d4db549e780d047a3 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 21:14:57 -0400 Subject: [PATCH 27/30] De-duplicate common linux and android modules --- src/common/os/android.rs | 191 ++----------------------------- src/common/os/common.rs | 105 +++++++++++++++++ src/common/os/linux.rs | 211 ++--------------------------------- src/common/os/linux_sysfs.rs | 141 ++++++++++++++++++++++- 4 files changed, 265 insertions(+), 383 deletions(-) diff --git a/src/common/os/android.rs b/src/common/os/android.rs index 089cbeef..60e8ef5d 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -1,19 +1,12 @@ #![cfg(target_os = "android")] -use super::linux_sysfs::*; use crate::common::{ - CacheLevel, CacheType, DataSource, Level1Cache, OS, TDetect, TOSData, TopologyCount, - TopologyTier, cleanup_soc_vendor, format_compatible_pair, get_devicetree_compatible, - get_proc_cpuinfo_data, is_generic_value, + DataSource, OS, TOSData, TopologyTier, cleanup_soc_vendor, get_soc_from_devicetree, + get_soc_from_proc_cpuinfo, get_system_name_from_proc_cpuinfo, is_generic_value, + read_devicetree_string, }; use std::collections::HashMap; -#[cfg(any(not(x86_cpu), test))] -use crate::common::Cache; - -#[cfg(any(arm_cpu, test))] -use std::collections::BTreeMap; - // ---------------------------------------------------------------------------- // Android System Properties (Text-parsing of getprop) // ---------------------------------------------------------------------------- @@ -200,30 +193,6 @@ pub fn extract_soc(props: &HashMap) -> Option { None } -// ---------------------------------------------------------------------------- -// Helpers for CPU Lists & /proc/cpuinfo -// ---------------------------------------------------------------------------- - -fn get_soc_cpuinfo() -> Option { - let cpuinfo = get_proc_cpuinfo_data(); - if let Some(last) = cpuinfo.last() - && (!last.contains_key("processor")) - && let Some(raw_soc) = last.get("Hardware") - { - return Some(String::from(raw_soc.trim())); - } - None -} - -fn get_soc_devicetree() -> Option { - if let Some(raw_pairs) = get_devicetree_compatible() - && let Some(pair) = raw_pairs.last().cloned() - { - return Some(format_compatible_pair(pair)); - } - None -} - // ---------------------------------------------------------------------------- // TOSData Implementation // ---------------------------------------------------------------------------- @@ -235,11 +204,11 @@ impl TOSData for OS { return Some(soc); } - if let Some(soc) = get_soc_cpuinfo() { + if let Some(soc) = get_soc_from_proc_cpuinfo() { return Some(soc); } - if let Some(soc) = get_soc_devicetree() { + if let Some(soc) = get_soc_from_devicetree() { return Some(soc); } @@ -252,22 +221,12 @@ impl TOSData for OS { return Some(name); } - if let Some(last) = get_proc_cpuinfo_data().last() - && (!last.contains_key("processor")) - && let Some(raw) = last.get("Model") - && !is_generic_value(raw.trim()) - { - return Some(String::from(raw.trim())); + if let Some(name) = get_system_name_from_proc_cpuinfo() { + return Some(name); } - if let Ok(raw) = std::fs::read_to_string("/proc/device-tree/model") { - let raw: Vec<_> = raw.split('\0').collect(); - if let Some(first) = raw.first() { - let trimmed = first.trim(); - if !is_generic_value(trimmed) { - return Some(String::from(trimmed)); - } - } + if let Some(name) = read_devicetree_string("/proc/device-tree/model") { + return Some(name); } None @@ -278,138 +237,6 @@ impl TOSData for OS { } } -// ---------------------------------------------------------------------------- -// TopologyCount Detection -// ---------------------------------------------------------------------------- - -impl TDetect for TopologyCount { - fn detect() -> Self { - let sockets = OS::get_socket_count(); - let mut topo = detect_sysfs_topology(); - topo.sockets = sockets; - topo - } -} - -// ---------------------------------------------------------------------------- -// Cache Detection -// ---------------------------------------------------------------------------- - -#[cfg(any(not(x86_cpu), test))] -impl Cache { - #[cfg(not(x86_cpu))] - pub fn detect() -> Option { - if let Some(cache) = Self::from_sys_fs() { - return Some(cache); - } - - if let Some(cache) = Self::from_lscpu_command() { - return Some(cache); - } - - None - } - - pub(crate) fn from_sys_fs() -> Option { - read_sysfs_cpu_cache(0) - } - - /// Read cache info for each distinct CPU type (MIDR group) from sysfs. - #[cfg(any(arm_cpu, test))] - pub(crate) fn from_sys_fs_per_type() -> Option> { - read_sysfs_cache_per_type() - } - - #[cfg(not(x86_cpu))] - fn from_lscpu_command() -> Option { - let output = match std::process::Command::new("lscpu").arg("-C").output() { - Ok(o) => o.stdout, - Err(_) => return None, - }; - - let output_str = match String::from_utf8(output) { - Ok(s) => s, - Err(_) => return None, - }; - - let mut cache = Cache { - source: DataSource::Lscpu, - ..Default::default() - }; - let mut found_cache = false; - - let lines: Vec<&str> = output_str.lines().collect(); - if lines.len() < 2 { - return None; - } - - let table_keys: Vec<&str> = lines[0].split_whitespace().collect(); - - for line in lines.into_iter().skip(1) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() <= 3 { - continue; - } - - let name = parts[table_keys.iter().position(|&x| x == "NAME")?]; - let size_str = parts[table_keys.iter().position(|&x| x == "ONE-SIZE")?]; - let ways_str = parts[table_keys.iter().position(|&x| x == "WAYS")?]; - - let size_bytes: u32 = if let Some(stripped) = size_str.strip_suffix('K') { - stripped.parse::().ok()? * 1024 - } else if let Some(stripped) = size_str.strip_suffix('M') { - stripped.parse::().ok()? * 1024 * 1024 - } else { - size_str.parse::().ok()? * 1024 - }; - - let ways: u32 = ways_str.parse().unwrap_or(0); - - match name { - "L1d" => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::new(size_bytes, CacheType::Data, ways, 0), - instruction: CacheLevel::default(), - }; - found_cache = true; - } - "L1i" => { - if let Level1Cache::Split { instruction, .. } = &mut cache.l1 { - instruction.size = size_bytes; - instruction.kind = CacheType::Instruction; - instruction.assoc = ways; - } - } - "L1" => { - cache.l1 = Level1Cache::Unified(CacheLevel::new_unified(size_bytes, ways)); - found_cache = true; - } - "L2" => { - cache.l2 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); - found_cache = true; - } - "L3" => { - cache.l3 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); - found_cache = true; - } - _ => {} - } - } - - if let Level1Cache::Split { data, instruction } = &cache.l1 - && instruction.size == 0 - && data.size > 0 - { - cache.l1 = Level1Cache::Split { - data: *data, - instruction: CacheLevel::new(data.size, CacheType::Instruction, data.assoc, 0), - }; - } - - if found_cache { Some(cache) } else { None } - } -} - // ---------------------------------------------------------------------------- // Unit Tests // ---------------------------------------------------------------------------- diff --git a/src/common/os/common.rs b/src/common/os/common.rs index cd3efc38..abce7e73 100644 --- a/src/common/os/common.rs +++ b/src/common/os/common.rs @@ -267,6 +267,43 @@ pub fn for_each_logical_core(mut f: F) { } } +/// Reads the `Hardware` SoC name from the trailing non-processor block of `/proc/cpuinfo`. +#[cfg(std_os)] +pub fn get_soc_from_proc_cpuinfo() -> Option { + let cpuinfo = get_proc_cpuinfo_data(); + if let Some(last) = cpuinfo.last() + && (!last.contains_key("processor")) + && let Some(raw_soc) = last.get("Hardware") + { + return Some(String::from(raw_soc.trim())); + } + None +} + +/// Reads the `Model` system name from the trailing non-processor block of `/proc/cpuinfo`. +#[cfg(std_os)] +pub fn get_system_name_from_proc_cpuinfo() -> Option { + if let Some(last) = get_proc_cpuinfo_data().last() + && (!last.contains_key("processor")) + && let Some(raw) = last.get("Model") + && !is_generic_value(raw.trim()) + { + return Some(String::from(raw.trim())); + } + None +} + +/// Resolves SoC identity from the last entry of `/proc/device-tree/compatible`. +#[cfg(std_os)] +pub fn get_soc_from_devicetree() -> Option { + if let Some(raw_pairs) = get_devicetree_compatible() + && let Some(pair) = raw_pairs.last().cloned() + { + return Some(format_compatible_pair(pair)); + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -306,4 +343,72 @@ mod tests { assert_eq!(parse_frequency_mhz("1500"), Some(1500)); assert_eq!(parse_frequency_mhz(""), None); } + + #[test] + fn test_is_generic_value_placeholders() { + for value in [ + "To Be Filled By O.E.M.", + "To Be Filled", + "System Product Name", + "System Name", + "Product Name", + "All Series", + "Default string", + "Not Specified", + "Not Applicable", + "Unknown", + "Generic", + "OEM", + "O.E.M.", + ] { + assert!(is_generic_value(value), "{value:?} should be generic"); + } + } + + #[test] + fn test_is_generic_value_whitespace_and_case() { + assert!(is_generic_value(" DEFAULT STRING ")); + assert!(is_generic_value("to be filled by o.e.m.")); + assert!(is_generic_value("\tSystem Product Name\n")); + assert!(is_generic_value("")); + } + + #[test] + fn test_is_generic_value_real_names() { + for value in [ + "ThinkPad X1 Carbon", + "HP Spectre x360", + "MacBookPro18,3", + "Dell XPS 13 9310", + "QEMU Standard PC (i440FX + PIIX, 1996)", + "Orange Pi 5", + ] { + assert!(!is_generic_value(value), "{value:?} should be real"); + } + } + + #[test] + fn test_is_known_hypervisor_vendor() { + for vendor in [ + "QEMU", + "VMware, Inc.", + "innotek GmbH", + "Microsoft Corporation", + ] { + assert!(is_known_hypervisor_vendor(vendor)); + } + for vendor in ["Dell Inc.", "ASUSTeK COMPUTER INC.", "LENOVO"] { + assert!(!is_known_hypervisor_vendor(vendor)); + } + } + + #[test] + fn test_normalize_for_compare() { + assert_eq!( + normalize_for_compare(" Default String "), + "default string" + ); + assert_eq!(normalize_for_compare("QEMU"), "qemu"); + assert_eq!(normalize_for_compare(""), ""); + } } diff --git a/src/common/os/linux.rs b/src/common/os/linux.rs index bf825034..cec878a8 100644 --- a/src/common/os/linux.rs +++ b/src/common/os/linux.rs @@ -1,32 +1,12 @@ #![cfg(target_os = "linux")] -use super::linux_sysfs::*; use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, format_compatible_pair, - get_devicetree_compatible, get_proc_cpuinfo_data, + DataSource, OS, TOSData, TopologyTier, format_compatible_pair, get_devicetree_compatible, + get_proc_cpuinfo_data, get_soc_from_devicetree, get_soc_from_proc_cpuinfo, + get_system_name_from_proc_cpuinfo, read_devicetree_string, }; use std::collections::HashSet; -#[cfg(any(not(x86_cpu), test))] -use crate::common::Cache; - -#[cfg(not(x86_cpu))] -use crate::common::{CacheLevel, CacheType, Level1Cache}; - -#[cfg(any(arm_cpu, test))] -use std::collections::BTreeMap; - -fn get_soc_cpuinfo() -> Option { - let cpuinfo = get_proc_cpuinfo_data(); - if let Some(last) = cpuinfo.last() - && (!last.contains_key("processor")) - && let Some(raw_soc) = last.get("Hardware") - { - return Some(String::from(raw_soc.trim())); - } - None -} - use super::{is_generic_value, is_known_hypervisor_vendor}; /// Read a DMI field from sysfs, trying both the virtual and class mount @@ -77,27 +57,12 @@ fn get_combined_dmi(fields: &[&str], vendor_only: bool) -> Option { } fn get_raw_system_name() -> Option { - // Let's look for a few possibilities that may have the formatted device name, - // or at least the easier to use system name - let simple_paths: Vec<_> = vec![ + for path in [ "/proc/device-tree/model", "/proc/device-tree/smbios/smbios/system/product", - ]; - - for path in simple_paths { - if let Ok(raw) = std::fs::read_to_string(path) { - let raw: Vec<_> = raw.split('\0').collect(); - let raw = raw.first(); - { - let raw = raw?; - let trimmed = raw.trim(); - - if is_generic_value(trimmed) { - continue; - } - - return Some(String::from(trimmed)); - } + ] { + if let Some(name) = read_devicetree_string(path) { + return Some(name); } } @@ -131,23 +96,13 @@ fn get_raw_system_name() -> Option { None } -fn get_soc_devicetree() -> Option { - if let Some(raw_pairs) = get_devicetree_compatible() - && let Some(pair) = raw_pairs.last().cloned() - { - return Some(format_compatible_pair(pair)); - } - - None -} - impl TOSData for OS { fn get_soc() -> Option { - if let Some(soc) = get_soc_cpuinfo() { + if let Some(soc) = get_soc_from_proc_cpuinfo() { return Some(soc); } - if let Some(soc) = get_soc_devicetree() { + if let Some(soc) = get_soc_from_devicetree() { return Some(soc); } @@ -155,22 +110,11 @@ impl TOSData for OS { } fn get_system_name() -> Option { - // Let's try /proc/cpuinfo first, as that will be formatted nicely - if let Some(last) = get_proc_cpuinfo_data().last() - && (!last.contains_key("processor")) - && let Some(raw) = last.get("Model") - && !is_generic_value(raw.trim()) - { - return Some(String::from(raw.trim())); - } - - let name = get_raw_system_name(); - - if name.is_some() { - return name; + if let Some(name) = get_system_name_from_proc_cpuinfo() { + return Some(name); } - None + get_raw_system_name() } fn get_socket_count() -> TopologyTier { @@ -206,137 +150,6 @@ impl TOSData for OS { } } -impl TDetect for TopologyCount { - fn detect() -> Self { - let sockets = OS::get_socket_count(); - let mut topo = detect_sysfs_topology(); - topo.sockets = sockets; - topo - } -} - -#[cfg(any(not(x86_cpu), test))] -impl Cache { - #[cfg(not(x86_cpu))] - pub fn detect() -> Option { - if let Some(cache) = Self::from_sys_fs() { - return Some(cache); - } - - if let Some(cache) = Self::from_lscpu_command() { - return Some(cache); - } - - None - } - - #[cfg(not(x86_cpu))] - pub(crate) fn from_sys_fs() -> Option { - read_sysfs_cpu_cache(0) - } - - /// Read cache info for each distinct CPU type (MIDR group). - #[cfg(any(arm_cpu, test))] - pub(crate) fn from_sys_fs_per_type() -> Option> { - read_sysfs_cache_per_type() - } - - #[cfg(not(x86_cpu))] - fn from_lscpu_command() -> Option { - let output = match std::process::Command::new("lscpu").arg("-C").output() { - Ok(o) => o.stdout, - Err(_) => return None, - }; - - let output_str = match String::from_utf8(output) { - Ok(s) => s, - Err(_) => return None, - }; - - let mut cache = Cache { - source: DataSource::Lscpu, - ..Default::default() - }; - let mut found_cache = false; - - let lines: Vec<&str> = output_str.lines().collect(); - - // No output from lscpu -C - if lines.len() < 2 { - return None; - } - - let table_keys: Vec<&str> = lines[0].split_whitespace().collect(); - - // @TODO: Properly parse table to account for missing values - for line in lines.into_iter().skip(1) { - let parts: Vec<&str> = line.split_whitespace().collect(); - if parts.len() <= 3 { - continue; - } - - let name = parts[table_keys.iter().position(|&x| x == "NAME")?]; - let size_str = parts[table_keys.iter().position(|&x| x == "ONE-SIZE")?]; - let ways_str = parts[table_keys.iter().position(|&x| x == "WAYS")?]; - - // Parse size (e.g., "32K", "256K", "4M") - let size_bytes: u32 = if let Some(stripped) = size_str.strip_suffix('K') { - stripped.parse::().ok()? * 1024 - } else if let Some(stripped) = size_str.strip_suffix('M') { - stripped.parse::().ok()? * 1024 * 1024 - } else { - size_str.parse::().ok()? * 1024 - }; - - let ways: u32 = ways_str.parse().unwrap_or(0); - - match name { - "L1d" => { - cache.l1 = Level1Cache::Split { - data: CacheLevel::new(size_bytes, CacheType::Data, ways, 0), - instruction: CacheLevel::default(), - }; - found_cache = true; - } - "L1i" => { - if let Level1Cache::Split { instruction, .. } = &mut cache.l1 { - instruction.size = size_bytes; - instruction.kind = CacheType::Instruction; - instruction.assoc = ways; - } - } - "L1" => { - cache.l1 = Level1Cache::Unified(CacheLevel::new_unified(size_bytes, ways)); - found_cache = true; - } - "L2" => { - cache.l2 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); - found_cache = true; - } - "L3" => { - cache.l3 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); - found_cache = true; - } - _ => {} - } - } - - // Handle case where L1 is split but L1i wasn't in the output - if let Level1Cache::Split { data, instruction } = &cache.l1 - && instruction.size == 0 - && data.size > 0 - { - // Copy data settings to instruction - cache.l1 = Level1Cache::Split { - data: *data, - instruction: CacheLevel::new(data.size, CacheType::Instruction, data.assoc, 0), - }; - } - - if found_cache { Some(cache) } else { None } - } -} - #[cfg(test)] mod tests { use super::super::normalize_for_compare; diff --git a/src/common/os/linux_sysfs.rs b/src/common/os/linux_sysfs.rs index f2644e2b..7a043368 100644 --- a/src/common/os/linux_sysfs.rs +++ b/src/common/os/linux_sysfs.rs @@ -1,8 +1,8 @@ #![cfg(linux_os)] use crate::common::{ - Cache, CacheLevel, CacheType, DataSource, Level1Cache, TopologyCount, TopologyTier, - expand_cpu_list, get_proc_cpuinfo_data, parse_cpu_list_count, + Cache, CacheLevel, CacheType, DataSource, Level1Cache, OS, TDetect, TOSData, TopologyCount, + TopologyTier, expand_cpu_list, get_proc_cpuinfo_data, parse_cpu_list_count, }; use std::collections::{BTreeMap, HashSet}; use std::fs; @@ -235,3 +235,140 @@ pub fn read_sysfs_cache_per_type() -> Option> { Some(cache_map) } } + +// ---------------------------------------------------------------------------- +// TopologyCount Detection +// ---------------------------------------------------------------------------- + +impl TDetect for TopologyCount { + fn detect() -> Self { + let sockets = OS::get_socket_count(); + let mut topo = detect_sysfs_topology(); + topo.sockets = sockets; + topo + } +} + +// ---------------------------------------------------------------------------- +// Cache Detection +// ---------------------------------------------------------------------------- + +#[cfg(any(not(x86_cpu), test))] +impl Cache { + #[cfg(not(x86_cpu))] + pub fn detect() -> Option { + if let Some(cache) = Self::from_sys_fs() { + return Some(cache); + } + + if let Some(cache) = Self::from_lscpu_command() { + return Some(cache); + } + + None + } + + #[cfg(not(x86_cpu))] + pub(crate) fn from_sys_fs() -> Option { + read_sysfs_cpu_cache(0) + } + + /// Read cache info for each distinct CPU type (MIDR group). + #[cfg(any(arm_cpu, test))] + pub(crate) fn from_sys_fs_per_type() -> Option> { + read_sysfs_cache_per_type() + } + + #[cfg(not(x86_cpu))] + fn from_lscpu_command() -> Option { + let output = match std::process::Command::new("lscpu").arg("-C").output() { + Ok(o) => o.stdout, + Err(_) => return None, + }; + + let output_str = match String::from_utf8(output) { + Ok(s) => s, + Err(_) => return None, + }; + + let mut cache = Cache { + source: DataSource::Lscpu, + ..Default::default() + }; + let mut found_cache = false; + + let lines: Vec<&str> = output_str.lines().collect(); + + // No output from lscpu -C + if lines.len() < 2 { + return None; + } + + let table_keys: Vec<&str> = lines[0].split_whitespace().collect(); + + // @TODO: Properly parse table to account for missing values + for line in lines.into_iter().skip(1) { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() <= 3 { + continue; + } + + let name = parts[table_keys.iter().position(|&x| x == "NAME")?]; + let size_str = parts[table_keys.iter().position(|&x| x == "ONE-SIZE")?]; + let ways_str = parts[table_keys.iter().position(|&x| x == "WAYS")?]; + + // Parse size (e.g., "32K", "256K", "4M") + let size_bytes: u32 = if let Some(stripped) = size_str.strip_suffix('K') { + stripped.parse::().ok()? * 1024 + } else if let Some(stripped) = size_str.strip_suffix('M') { + stripped.parse::().ok()? * 1024 * 1024 + } else { + size_str.parse::().ok()? * 1024 + }; + + let ways: u32 = ways_str.parse().unwrap_or(0); + + match name { + "L1d" => { + cache.l1 = Level1Cache::Split { + data: CacheLevel::new(size_bytes, CacheType::Data, ways, 0), + instruction: CacheLevel::default(), + }; + found_cache = true; + } + "L1i" => { + if let Level1Cache::Split { instruction, .. } = &mut cache.l1 { + instruction.size = size_bytes; + instruction.kind = CacheType::Instruction; + instruction.assoc = ways; + } + } + "L1" => { + cache.l1 = Level1Cache::Unified(CacheLevel::new_unified(size_bytes, ways)); + found_cache = true; + } + "L2" => { + cache.l2 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); + found_cache = true; + } + "L3" => { + cache.l3 = Some(CacheLevel::new(size_bytes, CacheType::Unified, ways, 0)); + found_cache = true; + } + _ => {} + } + } + + if let Level1Cache::Split { data, instruction } = &cache.l1 + && instruction.size == 0 + && data.size > 0 + { + cache.l1 = Level1Cache::Split { + data: *data, + instruction: CacheLevel::new(data.size, CacheType::Instruction, data.assoc, 0), + }; + } + + if found_cache { Some(cache) } else { None } + } +} From da979e80b4069d908e879bbdaea0bc026b3bf1d9 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Fri, 28 Aug 2026 22:07:20 -0400 Subject: [PATCH 28/30] Look for L3 cache to distinguish Northwood from Gallatin --- src/x86/vendor/intel.rs | 39 ++++++++++++++++++++++++++++++--------- tests/cpuid_dump_test.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/x86/vendor/intel.rs b/src/x86/vendor/intel.rs index 36f8bd4d..5542b846 100644 --- a/src/x86/vendor/intel.rs +++ b/src/x86/vendor/intel.rs @@ -362,6 +362,22 @@ impl Intel { } } + #[cfg(not(dos_real))] + fn disambiguate_0f_02h( + model: &str, + brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, + ) -> CpuArch { + let has_l3 = crate::common::Cache::detect() + .and_then(|c| c.l3) + .is_some_and(|l| l.size() > 0); + + if has_l3 || model.contains("Gallatin") || model.contains("Extreme") { + brand_arch(MicroArch::Northwood, "Gallatin", Some(N130)) + } else { + brand_arch(MicroArch::Northwood, "Northwood", Some(N130)) + } + } + #[cfg(not(dos_real))] fn modern_micro_arch( model: &str, @@ -406,15 +422,7 @@ impl Intel { // NetBurst (P4 / Xeon) (0, 15, 0, 0, _) => brand_arch(MicroArch::Willamette, "Willamette", Some(N180)), (0, 15, 0, 1, _) => brand_arch(MicroArch::Willamette, "Willamette/Foster", Some(N180)), - (0, 15, 0, 2, _) => brand_arch( - MicroArch::Northwood, - if model.contains("Xeon") { - "Gallatin" - } else { - "Northwood" - }, - Some(N130), - ), + (0, 15, 0, 2, _) => Self::disambiguate_0f_02h(model, brand_arch), (0, 15, 0, 3, _) => brand_arch(MicroArch::Prescott, "Prescott", Some(N90)), (0, 15, 0, 4, _) => brand_arch(MicroArch::Prescott, "Prescott/Potomac", Some(N90)), @@ -950,6 +958,19 @@ mod test { assert_eq!(arch_dmr.micro_arch, MicroArch::DiamondRapids); assert_eq!(arch_dmr.code_name, "Diamond Rapids-X"); + // NetBurst: Gallatin (0F_02H) via model string fallback + let sig_p4_0f02 = crate::x86::micro_arch::tests::dummy_signature(15, 2, 0, 0, 9); + let arch_gallatin_ee = Intel::micro_arch( + "Intel(R) Pentium(R) 4 Extreme Edition CPU 3.40GHz", + sig_p4_0f02, + ); + assert_eq!(arch_gallatin_ee.micro_arch, MicroArch::Northwood); + assert_eq!(arch_gallatin_ee.code_name, "Gallatin"); + + let arch_gallatin_str = Intel::micro_arch("Intel Gallatin Processor", sig_p4_0f02); + assert_eq!(arch_gallatin_str.micro_arch, MicroArch::Northwood); + assert_eq!(arch_gallatin_str.code_name, "Gallatin"); + // Unknown Intel let sig_unknown = crate::x86::micro_arch::tests::dummy_signature(99, 0, 0, 0, 0); let arch = Intel::micro_arch(model, sig_unknown); diff --git a/tests/cpuid_dump_test.rs b/tests/cpuid_dump_test.rs index 86b6dc59..83a2c0e7 100644 --- a/tests/cpuid_dump_test.rs +++ b/tests/cpuid_dump_test.rs @@ -1062,6 +1062,44 @@ cpuid_testsuite!( } ); +cpuid_testsuite!( + p4_northwood, + "dump/P4Northwood.txt", + { + test vendor_detection { + assert_vendor(VENDOR_INTEL); + } + + test brand_string { + assert_brand_contains("Pentium(R) 4"); + } + + test signature { + assert_eq!(get_signature(), (0, 15, 0, 2, 7)); + } + + test arch_detection { + let cpu = Cpu::detect(); + assert_eq!(cpu.extra.arch.micro_arch, MicroArch::Northwood); + assert_eq!(cpu.extra.arch.code_name, "Northwood"); + } + + test topology { + assert_topology(1, 1, 2); + } + + test cache_detection { + let cpu = Cpu::detect(); + let cache = cpu.topology.cache.expect("Expected cache to be detected"); + assert_eq!(cache.l1.size(), 8_192, "L1 data should be 8KB"); + assert!(cache.l3.is_none(), "Northwood should have no L3 cache"); + if let Some(l2) = cache.l2 { + assert_eq!(l2.size(), 524_288, "L2 should be 512KB"); + } + } + } +); + // ---------------------------------------------------------------------------- // ! Miscellaneous Tests // ---------------------------------------------------------------------------- From 5cc608553362751d4398eb88072410ac0169123b Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 31 Aug 2026 09:10:39 -0400 Subject: [PATCH 29/30] Add helper method for if cache has detected l3 --- src/common/cache.rs | 10 ++++++++++ src/x86/vendor/intel.rs | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/common/cache.rs b/src/common/cache.rs index 57a48d73..dff19e15 100644 --- a/src/common/cache.rs +++ b/src/common/cache.rs @@ -171,6 +171,16 @@ impl Cache { self.l1.size() == 0 && self.l2.is_none() && self.l3.is_none() } + pub fn has_l3(&self) -> bool { + if let Some(l3) = self.l3 + && l3.size > 0 + { + return true; + } + + false + } + /// Detects cache using platform/OS specific information sources. #[must_use] pub fn detect_os() -> Option { diff --git a/src/x86/vendor/intel.rs b/src/x86/vendor/intel.rs index 5542b846..6a28c143 100644 --- a/src/x86/vendor/intel.rs +++ b/src/x86/vendor/intel.rs @@ -367,9 +367,7 @@ impl Intel { model: &str, brand_arch: &impl Fn(MicroArch, &'static str, Option<&'static str>) -> CpuArch, ) -> CpuArch { - let has_l3 = crate::common::Cache::detect() - .and_then(|c| c.l3) - .is_some_and(|l| l.size() > 0); + let has_l3 = crate::common::Cache::detect().is_some_and(|c| c.has_l3()); if has_l3 || model.contains("Gallatin") || model.contains("Extreme") { brand_arch(MicroArch::Northwood, "Gallatin", Some(N130)) From 354dfbfdd5e30fec1f5ac6f1b2a917acc4bf0fb8 Mon Sep 17 00:00:00 2001 From: "Timothy J. Warren" Date: Mon, 31 Aug 2026 09:25:08 -0400 Subject: [PATCH 30/30] Update changelog --- CHANGELOG.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f0cd55..436eb6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,32 @@ # Changelog -## [2.1.0] — Unified topology model, multi-socket display, PowerPC fixes, and common deduplication +## [2.1.0] — Unified topology model, multi-socket display, NetBurst Gallatin detection, and common deduplication ### Added +- **NetBurst Northwood vs. Gallatin Disambiguation via L3 Cache**: Added `disambiguate_0f_02h` to distinguish Intel NetBurst Pentium 4 (Northwood, without L3 cache) from Pentium 4 Extreme Edition / Xeon MP (Gallatin, with L3 cache) using live L3 cache detection (`Cache::has_l3()`) and model name fallback (`src/x86/vendor/intel.rs`, `src/common/cache.rs`) +- **L3 Cache Helper Method**: Added `Cache::has_l3(&self) -> bool` method to easily check whether an L3 cache level is present and non-zero (`src/common/cache.rs`) - **DOS Colored Console Display**: Added driverless VGA text mode ANSI color rendering for 32-bit protected mode DOS (`dos32a` build, `rustid.exe`), matching output on CLI and UEFI targets; enabled `color: true` by default, added `/M` / `/MONO` flag support for monochrome output, and automatically stripped color escape sequences when output redirection to a file is detected (`src/dos_rustid.rs`, `src/x86/dos/mod.rs`) -- **Unified CPU Topology on Common Struct**: Extracted the `Topology` struct (`sockets`, `dies`, `cores`, `threads`, `speed`, `cache`) into `src/common/topology.rs` and placed `topology: Topology` directly on the shared `Cpu` object across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`) +- **Unified CPU Topology on Common Struct**: Extracted the `Topology` struct (`sockets`, `dies`, `cores`, `threads`, `speed`, `cache`) into `src/common/topology.rs` and placed `topology: Topology` directly on the shared `Cpu` generic object across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`) (`src/common/cpu.rs`, `src/common/topology.rs`) - **Multi-Socket Display Support**: Added physical socket count formatting across all architectures (`x86`, `ARM`, `RISC-V`, `PowerPC`); outputs `Topology: sockets, cores, threads` whenever physical socket count is greater than 1 or in verbose mode (`src/common/display.rs`) -- **Sysfs Socket Detection**: Added physical package ID parsing from `/sys/devices/system/cpu/cpu*/topology/physical_package_id` and `/proc/cpuinfo` `physical id` to automatically detect multi-socket systems on Linux/Android (`src/common/os/linux_sysfs.rs`) +- **Sysfs Socket Detection**: Added physical package ID parsing from `/sys/devices/system/cpu/cpu*/topology/physical_package_id` and `/proc/cpuinfo` `physical id` to automatically detect multi-socket systems on Linux and Android (`src/common/os/linux_sysfs.rs`) - **Single-Core SMT Display**: `display_topology_line` now explicitly displays thread count for single-core hyperthreaded CPUs (e.g. `1 core (2 threads)`) (`src/common/display.rs`) -- **Centralized Linux Sysfs Module**: Extracted sysfs cache tree traversal (`read_sysfs_cpu_cache`, `read_sysfs_cache_per_type`) and topology reader (`detect_sysfs_topology`) into a shared `linux_sysfs` module compiled under `#[cfg(linux_os)]` (`src/common/os/linux_sysfs.rs`) -- **Cross-Compilation CI & Target Checks**: Added `check-all` recipe to `justfile` and `Makefile` and integrated target compilation checks in GitHub Actions CI covering `x86_64-unknown-uefi`, `powerpc-unknown-linux-gnu`, `riscv64gc-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, and `aarch64-pc-windows-msvc` (`.github/workflows/ci.yml`, `justfile`, `Makefile`) -- **Common Device-Tree and Integer Frequency Helpers**: Added pure `core`/`no_std` safe integer frequency parser `parse_frequency_mhz`, devicetree helpers (`read_devicetree_string`, `read_devicetree_u64`), and thread affinity iterator `for_each_logical_core` (`src/common/os/common.rs`) -- **Xeon Dual-Socket Example**: Added dual-socket Pentium Pro / Xeon test fixture output (`examples/2PPRO.TXT`) +- **Centralized Linux & Android Sysfs Module**: Extracted sysfs cache tree traversal (`read_sysfs_cpu_cache`, `read_sysfs_cache_per_type`), `lscpu -C` cache fallback parser, and topology reader (`detect_sysfs_topology`) into a shared `linux_sysfs` module compiled under `#[cfg(any(target_os = "linux", target_os = "android"))]` (`src/common/os/linux_sysfs.rs`) +- **Common Device-Tree, Frequency, and OS Helpers**: Added pure `core`/`no_std` safe integer frequency parser `parse_frequency_mhz`, devicetree helpers (`read_devicetree_string`, `read_devicetree_u64`, `get_devicetree_compatible`, `format_compatible_pair`), CPU governor/status parsers (`parse_governor_from_str`, `parse_status_from_str`), and thread affinity iterator `for_each_logical_core` (`src/common/os/common.rs`) +- **Cross-Compilation CI & Target Checks**: Added `check-all` recipe to `justfile` and `Makefile` with auto-installation of missing `rustup` targets, and integrated target compilation checks in GitHub Actions CI covering `x86_64-unknown-uefi`, `i586-unknown-none` (DOS), `aarch64-linux-android`, `powerpc-unknown-linux-gnu`, `riscv64gc-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, and `aarch64-pc-windows-msvc` (`.github/workflows/push.yml`, `justfile`, `Makefile`) +- **ELF to LE Converter Section Handling**: Extended `elf2le` tool to match dotted ELF section names like `.rodata.*`, `.data.*`, and `.bss.*` for DOS32A protected-mode binaries (`tools/elf2le/src/main.rs`) +- **Xeon & Pentium 4 Test Fixtures**: Added dual-socket Intel Xeon E5-2470 test fixture (`examples/xeon-e5-2470.txt`) and Intel Pentium 4 Northwood CPUID dump and tests (`tests/cpuid/dump/P4Northwood.txt`, `tests/cpuid_dump_test.rs`) ### Changed -- **Deduplicated ARM Linux & Android OS Detection**: Replaced separate `src/arm/os/android.rs` (345 lines) with unified `src/arm/os/linux.rs` guarded by `#[cfg(linux_os)]` +- **Deduplicated Linux & Android OS Detection**: Replaced separate `src/arm/os/android.rs` (345 lines) with unified `src/arm/os/linux.rs` guarded by `#[cfg(linux_os)]`, and consolidated duplicated sysfs and `/proc/cpuinfo` parsing logic across Linux and Android into `src/common/os/common.rs` and `src/common/os/linux_sysfs.rs` (`src/common/os/linux.rs`, `src/common/os/android.rs`) - **Separated CPUID Discovery from OS Enrichment**: Clarified separation between pure CPUID detection and live OS hardware enrichment (`enrich_cpu`) across DOS, EFI, and OS targets (`src/x86/cpu.rs`, `src/x86/os/mod.rs`, `src/x86/efi/mod.rs`, `src/x86/dos/mod.rs`) -- **Architecture Module Alignment**: Refactored ARM, RISC-V, and PowerPC modules to follow consistent structure and display patterns (`src/arm/cpu.rs`, `src/riscv/cpu.rs`, `src/ppc/cpu.rs`) +- **Architecture Module Alignment**: Refactored ARM, RISC-V, and PowerPC modules to follow consistent structure, shared generic CPU types, and unified display patterns (`src/arm/cpu.rs`, `src/riscv/cpu.rs`, `src/ppc/cpu.rs`, `src/ppc/micro_arch.rs`) +- **Release Workflow Cleanup**: Configured release workflow to clean untracked/dirty files before crates.io packaging and publishing (`.github/workflows/release.yml`) ### Fixed - **PowerPC Clock Speed Detection**: Fixed clock speed reporting on Linux PowerPC (e.g. PowerBook G4 / PowerBook5,2) by prioritizing `/proc/cpuinfo` `clock: MHz`, sysfs `cpufreq`, and CPU node devicetree clock (`/proc/device-tree/cpus/*/clock-frequency`) over root bus frequency (`/proc/device-tree/clock-frequency`, which reports the 166.66 MHz FSB bus clock) (`src/ppc/cpu.rs`) - **DOS Multi-Socket MP Table Detection**: Corrected DOS Intel MP Table topology calculation: MP Table entries represent logical processors (APIC IDs) rather than physical sockets, resolving false multi-socket reports on multi-core processors (e.g. Core 2 Quad previously reporting 4 sockets, 16 cores, 16 threads) (`src/x86/dos/mod.rs`, `src/x86/dos/mp.rs`) -- **Cross-Platform Thread Affinity**: Unified logical core affinity switching loop in ARM Linux, Windows, and BSD to use `for_each_logical_core` +- **Cross-Platform Thread Affinity**: Unified logical core affinity switching loop in ARM Linux, Windows, and BSD to use `for_each_logical_core` (`src/arm/os/mod.rs`, `src/common/os/common.rs`) +- **Android Target Build**: Resolved compile errors and missing imports when targeting `aarch64-linux-android` (`src/common/os/android.rs`) ## [2.0.0] — Add missing Intel and AMD cpu mappings, fix edge cases, and more