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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e99673b1..436eb6f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [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` 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 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 & 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 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, 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` (`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 ### Added 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" diff --git a/Makefile b/Makefile index 7b921e8a..557677da 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,14 +40,53 @@ 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: + @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 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-android check-486 + # More in-depth code style checking lint: cargo clippy --all-targets --all-features @@ -77,7 +116,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 @@ -87,7 +126,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/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/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 + diff --git a/justfile b/justfile index e44faf7d..56613229 100644 --- a/justfile +++ b/justfile @@ -19,14 +19,53 @@ _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: + @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 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-android check-486 + # More in-depth code style checking lint: cargo clippy --all-targets --all-features @@ -56,7 +95,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 @@ -66,7 +105,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/src/arm/cpu.rs b/src/arm/cpu.rs index 7e1adc49..635a0c74 100644 --- a/src/arm/cpu.rs +++ b/src/arm/cpu.rs @@ -4,38 +4,51 @@ 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: BTreeMap<(CoreType, Midr), CpuCore>, pub raw: BTreeMap, - pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } +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, + }; + + 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 c71e3758..720f3d01 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.values() { + 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.code_name + if let Some(cname) = &core.name && cname != UNK && !cname.is_empty() - && is_duplicate(model, cname) + && Self::is_duplicate(model, cname) { return false; } @@ -59,17 +50,17 @@ 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() { - let Some(cname) = &core.code_name else { + for (i, core) in cpu_info.cores.iter().enumerate() { + let Some(cname) = &core.name else { common_cname = None; break; }; 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) { + if ma_str != UNK && Self::is_duplicate(cname, &ma_str) { common_cname = None; break; } @@ -110,21 +101,21 @@ 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; } // 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) { + if ma_str == UNK || !Self::is_duplicate(code_name, &ma_str) { all_match_ma = false; break; } @@ -141,7 +132,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() { @@ -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 @@ -161,7 +152,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(); @@ -170,33 +161,38 @@ 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); + + 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(), + ); } - #[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}"); - println!("{}", disp.label(&core_num)); + if cpu_info.is_hybrid() { + for (i, core) in cpu_info.cores.iter().enumerate() { + disp.core_heading(i); - let vendor_str: &str = core.implementer.into(); - if 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); @@ -206,62 +202,52 @@ impl CpuDisplay { disp.section_line("MicroArch", &ma_str); } - if Self::should_show_core_codename(core, cpu_info, flags.verbose) - && let Some(codename) = &core.code_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()); - let cc = |s| CpuDisplay::cache_count(s, core.count); - disp.display_cache(core.cache, &cc, 0); + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); + + disp.display_core_cache(core.cache, core.count, sockets); if core.cache.is_none() { disp.newline(); } - - i += 1; } - } else if let Some(core) = cpu_info.cores.values().next() { - println!("{}", disp.label("Cores")); + } else if let Some(core) = cpu_info.cores.first() { + disp.print_label("Cores"); - let vendor_str: &str = core.implementer.into(); - if 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.code_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()); - let cc = |s| CpuDisplay::cache_count(s, core.count); - disp.display_cache(core.cache, &cc, 0); + disp.display_frequency(core.speed, flags); + + disp.display_core_cache(core.cache, core.count, sockets); } // 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"], + ); } } @@ -292,7 +278,7 @@ impl TCpuDisplay for Cpu { } fn display_table(&self, flags: CliFlags) { - CpuDisplay::display(self, flags); + CpuDisplay::display_arm(self, flags); } } @@ -302,34 +288,39 @@ 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, - }, - ); + let impl_str = if implementer != Vendor::Unknown { + Some(Into::<&str>::into(implementer).to_string()) + } else { + None + }; + cores.push(CpuCore { + kind, + micro_arch: ma, + name: cname.map(String::from), + implementer: impl_str, + cache: None, + speed: None, + count: 4, + threads: 4, + }); } 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, @@ -339,21 +330,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] @@ -498,14 +489,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 +510,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 +526,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 +537,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 )); @@ -574,24 +550,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 @@ -607,17 +587,11 @@ 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"); - - assert_eq!(gold.implementer, Vendor::Qualcomm); - assert_eq!(silver.implementer, Vendor::Arm); + let gold = &cpu_snapdragon.cores[0]; + let silver = &cpu_snapdragon.cores[1]; + + 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, @@ -642,7 +616,7 @@ mod tests { compact: false, verbose: false, }; - CpuDisplay::display(&cpu, flags); + CpuDisplay::display_arm(&cpu, flags); } #[test] @@ -660,7 +634,7 @@ mod tests { compact: false, verbose: false, }; - CpuDisplay::display(&cpu, flags); + CpuDisplay::display_arm(&cpu, flags); } #[test] @@ -670,7 +644,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/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/micro_arch.rs b/src/arm/micro_arch.rs index e80779bf..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, UNK}; pub const IMPLEMENTER_MASK: usize = 0xFF000000; pub const VARIANT_MASK: usize = 0x00F00000; @@ -48,15 +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 count: u32, -} +pub type CpuCore = crate::common::CpuCore; /// ARM Microarchitectures across vendors. /// 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/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/macos.rs b/src/arm/os/macos.rs index a7962d00..3080c153 100644 --- a/src/arm/os/macos.rs +++ b/src/arm/os/macos.rs @@ -417,12 +417,14 @@ 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, + 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..668880e9 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}; @@ -7,7 +9,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,12 +20,12 @@ 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(); - #[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(); @@ -33,14 +35,14 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore 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); @@ -59,16 +61,27 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore 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| c.count += 1) + .and_modify(|c| { + c.count += 1; + 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, + threads: 1, }); } @@ -78,7 +91,7 @@ pub(crate) fn detect_cores(midrs: &[Midr]) -> BTreeMap<(CoreType, Midr), CpuCore } } - cores + cores.into_values().collect() } // ---------------------------------------------------------------------------- @@ -91,30 +104,21 @@ pub mod macos; pub use macos::*; // ---------------------------------------------------------------------------- -// ! Android -// ---------------------------------------------------------------------------- - -#[cfg(target_os = "android")] -pub mod android; -#[cfg(target_os = "android")] -pub use android::*; - -// ---------------------------------------------------------------------------- -// ! Linux +// ! Linux / Android // ---------------------------------------------------------------------------- -#[cfg(target_os = "linux")] +#[cfg(linux_os)] pub mod linux; -#[cfg(target_os = "linux")] +#[cfg(linux_os)] pub use linux::*; // ---------------------------------------------------------------------------- // ! Windows // ---------------------------------------------------------------------------- -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub mod windows; -#[cfg(target_os = "windows")] +#[cfg(windows_os)] pub use windows::*; // ---------------------------------------------------------------------------- @@ -129,7 +133,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() { @@ -167,19 +171,21 @@ 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); - 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 - .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); - assert_eq!(gold_core.implementer, Vendor::Arm); + assert_eq!(gold_core.implementer.as_deref(), Some("ARM")); assert_eq!(gold_core.micro_arch, MicroArch::ArmCortexA77); } @@ -218,15 +224,17 @@ 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); + assert_eq!(silver.implementer.as_deref(), Some("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); + assert_eq!(gold.implementer.as_deref(), Some("Qualcomm")); } } diff --git a/src/arm/os/windows.rs b/src/arm/os/windows.rs index e0352dd2..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(); @@ -242,7 +234,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/cache.rs b/src/common/cache.rs index 7eea290e..dff19e15 100644 --- a/src/common/cache.rs +++ b/src/common/cache.rs @@ -171,21 +171,24 @@ 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 { - #[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/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 a80f2a77..8f5de58c 100644 --- a/src/common/display.rs +++ b/src/common/display.rs @@ -1,12 +1,27 @@ +use super::Speed; use super::cache::{Cache, Level1Cache}; 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, } @@ -67,6 +82,100 @@ 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 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 && threads == 0 { + return String::new(); + } + 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. + 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); @@ -81,7 +190,7 @@ impl CpuDisplay { } pub fn newline(&self) { - #[cfg(not(any(dos, dos32a)))] + #[cfg(not(dos_os))] if !self.flags.compact { println!(); } @@ -98,6 +207,107 @@ 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, + 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", + socket_prefix, + Self::format_core_threads(total_cores, total_threads), + cluster_count + ), + ); + } else if total_cores > 0 { + 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), + ); + } + } + } + + /// 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() { + 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) { + 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() @@ -115,6 +325,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, @@ -224,15 +439,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 @@ -693,4 +901,97 @@ 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 (4 threads)"); + assert_eq!(CpuDisplay::format_core_threads(4, 8), "4 cores (8 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] + 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); + } + + #[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); + } + + #[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/common/mod.rs b/src/common/mod.rs index ee4730bf..f024f7db 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,357 +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, PartialEq)] -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(), - } - } -} - -#[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/android.rs b/src/common/os/android.rs index 0f8d6e28..60e8ef5d 100644 --- a/src/common/os/android.rs +++ b/src/common/os/android.rs @@ -1,17 +1,11 @@ #![cfg(target_os = "android")] use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, cleanup_soc_vendor, - 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, HashSet}; -use std::fs; -use std::path::Path; - -use crate::common::{Cache, CacheLevel, CacheType, Level1Cache}; - -#[cfg(any(arm_cpu, test))] -use std::collections::BTreeMap; +use std::collections::HashMap; // ---------------------------------------------------------------------------- // Android System Properties (Text-parsing of getprop) @@ -199,123 +193,6 @@ pub fn extract_soc(props: &HashMap) -> Option { None } -// ---------------------------------------------------------------------------- -// 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() - && (!last.contains_key("processor")) - && let Some(raw_soc) = last.get("Hardware") - { - return Some(String::from(raw_soc.trim())); - } - 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() - { - return Some(format_compatible_pair(pair)); - } - 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 // ---------------------------------------------------------------------------- @@ -327,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); } @@ -344,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 @@ -370,340 +237,6 @@ impl TOSData for OS { } } -// ---------------------------------------------------------------------------- -// TopologyCount Detection -// ---------------------------------------------------------------------------- - -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; - } - - topo - } -} - -// ---------------------------------------------------------------------------- -// Cache Detection -// ---------------------------------------------------------------------------- - -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 { - 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 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) - } - } - - #[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 c9f37215..abce7e73 100644 --- a/src/common/os/common.rs +++ b/src/common/os/common.rs @@ -67,3 +67,348 @@ 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() +} + +/// Parse a frequency string (e.g. "3.2 GHz", "800 MHz", "2400.00", "1.5GHz") into MHz as u64. +pub fn parse_frequency_mhz(value: &str) -> 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(); + } +} + +/// 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::*; + + #[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"); + } + + #[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); + } + + #[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/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/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/linux.rs b/src/common/os/linux.rs index 0c5305d4..cec878a8 100644 --- a/src/common/os/linux.rs +++ b/src/common/os/linux.rs @@ -1,111 +1,11 @@ #![cfg(target_os = "linux")] use crate::common::{ - DataSource, OS, TDetect, TOSData, TopologyCount, TopologyTier, cleanup_soc_vendor, + 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::{HashMap, HashSet}; -use std::fs; -use std::path::Path; - -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() - && (!last.contains_key("processor")) - && let Some(raw_soc) = last.get("Hardware") - { - return Some(String::from(raw_soc.trim())); - } - 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 std::collections::HashSet; use super::{is_generic_value, is_known_hypervisor_vendor}; @@ -157,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); } } @@ -211,44 +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 -} - -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() { + 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); } @@ -256,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 { @@ -307,331 +150,11 @@ 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; - } - - topo - } -} - -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 { - 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 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) - } - } - - #[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; 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..7a043368 --- /dev/null +++ b/src/common/os/linux_sysfs.rs @@ -0,0 +1,374 @@ +#![cfg(linux_os)] + +use crate::common::{ + 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; +use std::path::Path; + +/// Detects topology counts (sockets, 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(); + let mut package_ids = HashSet::new(); + for cpu_id in cpus { + 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 { + 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; + } + + 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 +} + +/// 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) + } +} + +// ---------------------------------------------------------------------------- +// 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 } + } +} diff --git a/src/common/os/mod.rs b/src/common/os/mod.rs index 947490d4..cc520b63 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; @@ -25,19 +25,22 @@ pub mod sysctl; #[cfg(any(target_os = "haiku", test))] pub mod haiku; -#[cfg(target_os = "windows")] +#[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::*; -#[cfg(target_os = "linux")] -pub use linux::*; - #[cfg(target_os = "macos")] pub use macos::*; 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/dos_rustid.rs b/src/dos_rustid.rs index 3930df98..b0ef5bbf 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]"); @@ -36,12 +36,13 @@ 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"); } -#[cfg(dos32a)] +#[cfg(dos_ext)] #[unsafe(no_mangle)] pub extern "C" fn rust_main() -> ! { use rustid::common::{CliFlags, TCpuDisplay, TDetect}; @@ -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", @@ -209,5 +218,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 3a33997d..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; @@ -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; @@ -71,18 +71,18 @@ pub mod arm; #[cfg(arm_cpu)] pub use arm::Cpu; -#[cfg(target_arch = "riscv64")] +#[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/ppc/cpu.rs b/src/ppc/cpu.rs index 8f83388d..0bacfc41 100644 --- a/src/ppc/cpu.rs +++ b/src/ppc/cpu.rs @@ -4,30 +4,64 @@ 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, Topology, TopologyTier, UNK}; +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 cache: Option, - pub clock_speed: Option, pub clock_speed_source: DataSource, } -impl Default for Cpu { - fn default() -> Self { - Self::detect() - } -} +pub type Cpu = crate::common::Cpu; impl Cpu { + 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() + .filter(|m| m.contains_key("processor")) + .count() as u32; + + let thread_count = if proc_count > 0 { + proc_count + } else if sysfs_topo.threads > 0 { + sysfs_topo.threads + } else { + 1 + }; + + let sockets = if sysfs_topo.sockets.count > 0 { + sysfs_topo.sockets.count + } else { + 1 + }; + + // 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 (sockets, core_count, thread_count); + } + + (sockets, thread_count, thread_count) + } + #[cfg(not(target_os = "linux"))] + { + (1, 1, 1) + } + } + fn detect_cache() -> Option { #[cfg(any(target_os = "linux", target_family = "unix"))] if let Some(cache) = Cache::detect() { @@ -40,41 +74,67 @@ 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 Ok(freq_str) = fs::read_to_string(dt_root.join("clock-frequency")) { - if let Ok(freq_hz) = freq_str.trim().parse::() { - 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 Ok(freq_str) = fs::read_to_string(dt_root.join("timebase-frequency")) { - if let Ok(freq_hz) = freq_str.trim().parse::() { - 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); + } + } } } @@ -93,47 +153,34 @@ 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) { - return Some(freq); - } - } + if (line.starts_with("CPU max MHz") || line.starts_with("CPU MHz")) + && let Some(value) = line.split(':').nth(1) + && let Some(freq) = crate::common::parse_frequency_mhz(value) + { + return Some(freq); } } None } + #[cfg(target_os = "linux")] 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) { - return Some(freq); - } + 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); } } 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() + #[cfg(not(target_os = "linux"))] + fn detect_clock_speed_from_cpuinfo() -> Option { + None } } @@ -144,21 +191,119 @@ impl TDetect for Cpu { let version = (pvr >> 16) as u16; let revision = (pvr & 0xFFFF) as u16; let cpu_arch = CpuArch::find(pvr); + 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(1, 1, 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 { + base: mhz as u32, + boost: mhz as u32, + measured: false, + }); - Self { - system, + 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 + }, + implementer: None, + cache, + speed, + count: core_count, + threads: thread_count, + }]; + + let extra = PpcData { pvr, version, revision, cpu_arch, - cache, - clock_speed, clock_speed_source, + }; + + let vendor = String::from(if extra.cpu_arch.marketing_name != UNK { + "IBM" + } else { + 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, } } } + +#[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)); + } +} diff --git a/src/ppc/display.rs b/src/ppc/display.rs index 2dd9b60f..1e42b3a9 100644 --- a/src/ppc/display.rs +++ b/src/ppc/display.rs @@ -19,22 +19,27 @@ 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); - } - - if let Some(clock_mhz) = self.clock_speed { - println!( - "{}{}", - disp.label("Frequency"), - CpuDisplay::format_frequency(clock_mhz) + disp.simple_line_opt("Process", self.cpu_arch.technology); + + let total_cores = self.total_cores(); + let total_threads = self.total_threads(); + 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(), ); - disp.newline(); } - // 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() { + disp.display_frequency(core.speed, flags); + + 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 bf55e902..3f93fb27 100644 --- a/src/ppc/micro_arch.rs +++ b/src/ppc/micro_arch.rs @@ -1,5 +1,7 @@ use crate::common::constants::*; +pub type CpuCore = crate::common::CpuCore; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MicroArch { Unknown, @@ -215,7 +217,9 @@ impl CpuArch { #[cfg(test)] mod tests { + use super::CpuCore; use super::*; + use crate::common::Topology; #[test] fn test_ppc_find_classic() { @@ -306,4 +310,45 @@ 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, 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, + boost: 2000, + measured: false, + }), + count: 2, + threads: 2, + }; + + let cpu = Cpu { + 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 { + pvr: 0x0039_0202, + version: 0x0039, + revision: 0x0202, + cpu_arch: CpuArch::find(0x0039_0202), + 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 } diff --git a/src/riscv/cpu.rs b/src/riscv/cpu.rs index 3eea99d8..679f337b 100644 --- a/src/riscv/cpu.rs +++ b/src/riscv/cpu.rs @@ -3,36 +3,49 @@ 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: BTreeMap, pub raw: BTreeMap, - pub features: BTreeMap<&'static str, String>, pub midr_source: DataSource, pub features_source: DataSource, } +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, + }; + + 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, } } } @@ -46,6 +59,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..2768fe07 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(); @@ -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,29 +32,56 @@ 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 - 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(); - } + 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, + cpu_info.cores.len(), + ); + + for (i, core) in cpu_info.cores.iter().enumerate() { + disp.core_heading(i); + + let type_str: &str = core.kind.into(); + disp.section_line("Type", type_str); + + disp.section_line_opt("MicroArch", core.name.as_deref()); + + disp.section_line("Count", &core.count.to_string()); + + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); - // 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); + 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(sockets, core.count, core.threads, false, 1); + + disp.display_core_cache(core.cache, core.count, sockets); + + 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", @@ -65,19 +92,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 9f48aacb..0e676805 100644 --- a/src/riscv/micro_arch.rs +++ b/src/riscv/micro_arch.rs @@ -4,17 +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, UNK}; use crate::riscv::brand::*; -#[derive(Debug, Clone, PartialEq)] -pub struct CpuCore { - pub kind: CoreType, - pub name: Option, - pub cache: Option, - pub count: u32, -} +pub type CpuCore = crate::common::CpuCore; /// RISC-V `misa` register layout. /// @@ -43,7 +37,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 +503,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..3443d9ca 100644 --- a/src/riscv/mod.rs +++ b/src/riscv/mod.rs @@ -1,4 +1,4 @@ -#![cfg(target_arch = "riscv64")] +#![cfg(any(riscv_cpu, test))] //! RISC-V CPU detection. pub mod brand; diff --git a/src/riscv/os/linux.rs b/src/riscv/os/linux.rs index e004a13c..d0af42b3 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,20 @@ 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)), + implementer: None, + cache, + speed, + count: core_count, + threads: core_count, + }] } else { - BTreeMap::new() + vec![] }; OsCpuInfo { @@ -219,6 +218,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..361a70cd 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; @@ -8,7 +9,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, @@ -18,7 +19,29 @@ 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(linux_os))] +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(linux_os))] +pub use fallback::*; 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 c6403218..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,80 +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(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 0606644a..c274125a 100644 --- a/src/x86/cpu.rs +++ b/src/x86/cpu.rs @@ -2,21 +2,18 @@ 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::*; -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 alloc::collections::BTreeMap; +#[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(any(not(nostd_os), target_os = "uefi"))] -use crate::common::TOSData; - /// CPU feature class/level enumeration. /// /// Represents the instruction set and feature level of an x86 processor, @@ -172,9 +169,9 @@ impl CpuSignature { let is_overdrive = super::is_overdrive(); Self { - extended_model, extended_family, family, + extended_model, model, stepping, display_family, @@ -190,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; @@ -204,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; @@ -229,29 +226,9 @@ impl CpuSignature { } } -/// Information about a specific core type/cluster in the CPU. -#[derive(Debug, Default, 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, - /// 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. -#[derive(Debug, Default, PartialEq)] -pub struct Cpu { - /// The system name, if applicable - #[cfg(any(not(nostd_os), target_os = "uefi"))] - pub system: Option, +/// Extended x86-specific CPU information. +#[derive(Debug, PartialEq, Default)] +pub struct X86Data { /// Does this cpu have cpuid instruction support pub has_cpuid: bool, /// CPU architecture and microarchitecture details @@ -264,21 +241,20 @@ 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, + /// Topology domains discovered from CPUID leaves + pub topology_domains: DomainList, } +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 { 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(); @@ -329,7 +305,7 @@ impl Cpu { } } - #[cfg(not(dos))] + #[cfg(not(dos_real))] fn cleanup_model_string(s: &str) -> String { let str = s.replace("CPU", ""); @@ -383,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); } @@ -499,14 +475,14 @@ impl Cpu { } }; - #[cfg(not(dos))] + #[cfg(not(dos_real))] return Self::cleanup_model_string(s); - #[cfg(dos)] + #[cfg(dos_real)] String::from(s) } - fn easter_egg() -> Option { + pub(crate) fn easter_egg() -> Option { let mut out: String = String::new(); let brand = CpuBrand::detect(); @@ -550,40 +526,40 @@ 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(any(not(nostd_os), target_os = "uefi"))] - let system = { - #[cfg(not(nostd_os))] - if provider::info_source() == provider::CpuidInfoSource::DumpFile { - None - } else { - crate::common::OS::get_system_name() - } + let mut cpu = Self::detect_cpuid(); - #[cfg(target_os = "uefi")] - { - crate::common::OS::get_system_name() - } - }; + #[cfg(std_os)] + if provider::info_source() == provider::CpuidInfoSource::Cpu { + super::os::enrich_cpu(&mut cpu); + } - let sig = CpuSignature::detect(); - let arch = CpuArch::find(&Self::raw_model_string(), sig, &vendor_str()); - let topology = Topology::detect(); + #[cfg(uefi)] + super::efi::enrich_cpu(&mut cpu); - #[cfg(any(not(nostd_os), target_os = "uefi"))] - let cores = if is_intel() { - Self::detect_core_types() - } else { - Vec::new() - }; + #[cfg(dos_os)] + super::dos::enrich_cpu(&mut cpu); - #[cfg(all(nostd_os, not(target_os = "uefi")))] - let cores = Vec::new(); + cpu + } +} - Self { - #[cfg(any(not(nostd_os), target_os = "uefi"))] - system, +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 topology_domains = Topology::detect_domains(); + let cores = Self::detect_cpuid_core_types(&arch, &topology); + + 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 { @@ -594,57 +570,45 @@ impl TDetect for Cpu { easter_egg: Self::easter_egg(), brand_id: get_brand_id(), signature: sig, - features: get_feature_list(), + topology_domains, + }; + + Self { + system: None, + vendor: String::from(extra.arch.brand_name), + model: extra.arch.model.clone(), topology, cores, + features: get_feature_list(), + extra, } } -} -#[cfg(any(not(nostd_os), target_os = "uefi"))] -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_type: CoreType, - name: Option<&'static str>, - micro_arch: MicroArch, - cache: Option, - count: u32, - threads: u32, - ) { - if let Some(c) = cores - .iter_mut() - .find(|c| c.kind == core_type && c.name == name) - { - c.count += count; - c.threads += threads; - } else { - cores.push(CpuCore { - kind: core_type, - micro_arch, - name, - cache, - count, - threads, - }); + /// 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); + } } - } - #[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); @@ -664,96 +628,120 @@ 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 }; - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 1); + 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, + }, + ); } - return cores; - } - - #[cfg(not(nostd_os))] - if let Some(core_ids) = core_affinity::get_core_ids() { - let cache = Cache::detect(); - - 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(name_str) - } else { - None - }; - - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 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(); - 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(); - - mp.run_on_processor(cpu_idx, || { - core_type = core_type_from_cpuid(); - sig = CpuSignature::detect(); - raw_model = Cpu::raw_model_string(); - vendor = vendor_str(); - }); - - 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(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 + } - find_or_push(&mut cores, core_type, name, micro_arch, cache, 1, 1); - } + /// 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) + } + + /// 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 { - 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() + } } } @@ -776,24 +764,28 @@ 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, + ..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, + ..Default::default() + }, features: get_feature_list(), - topology: Topology::default(), ..Default::default() }; assert_eq!( @@ -803,33 +795,37 @@ 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, ..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), + ..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); @@ -839,16 +835,18 @@ 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), + ..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 2048e1dc..a80701ae 100644 --- a/src/x86/display.rs +++ b/src/x86/display.rs @@ -2,19 +2,15 @@ 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( @@ -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,52 +72,42 @@ 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.cores.is_empty() { - println!( - "{}{} cores ({} threads) across {} core types", - disp.label("Topology"), + if self.is_hybrid() { + disp.display_topology_line( + self.topology.sockets.count, self.topology.cores.count, self.topology.threads.count, - self.cores.len() + true, + 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(); - 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_opt("Codename", core.name.as_deref()); - if core.count != core.threads { - println!( - "{}{} cores ({} threads)", - disp.label("Topology"), - core.count, - core.threads - ); - } else { - println!("{}{} cores", disp.label("Topology"), core.count); - } + disp.section_line( + "Topology", + &CpuDisplay::format_core_threads(core.count, core.threads), + ); + + disp.display_frequency( + core.speed, + CliFlags { + compact: true, + ..flags + }, + ); let smt = cpuid_threads_per_core() .max(core.threads / core.count.max(1)) @@ -136,72 +122,29 @@ 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 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" - }; - - 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 - ); - } else { - println!("{lbl}{} cores", self.topology.cores.count); - } - - disp.newline(); + disp.display_topology_line( + self.topology.sockets.count, + self.topology.cores.count, + self.topology.threads.count, + false, + 1, + ); } } - fn print_speed(&self, disp: &CpuDisplay) { - if self.topology.speed.base > 0 { - let base = self.topology.speed.base; - let boost = self.topology.speed.boost; - - if boost > base { - println!( - "{}{}", - disp.inline_sublabel("Frequency", "Base"), - CpuDisplay::format_frequency(base) - ); - println!( - "{}{}", - disp.sublabel("Boost"), - CpuDisplay::format_frequency(boost) - ); - } else { - println!( - "{}{}", - disp.label("Frequency"), - CpuDisplay::format_frequency(base) - ); - } + fn print_speed(&self, flags: CliFlags, disp: &CpuDisplay) { + let speed = self + .cores + .first() + .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) { @@ -255,46 +198,9 @@ 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))] - fn print_centaur_features(&self, flags: CliFlags, disp: &CpuDisplay) { + #[cfg(not(dos_real))] + fn format_centaur_features(&self, flags: CliFlags) -> Option { use alloc::vec::Vec; let centaur_map = vendor::Centaur::get_feature_list(); @@ -305,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)")); } @@ -313,38 +219,38 @@ 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() { - // Simple features list - if self.features.len() == 1 { - self.print_simple_features_list(disp); - } else { - self.print_full_features_list(disp); - } + let mut features = self.features.clone(); - // Centaur features list - #[cfg(not(dos))] - if is_centaur() { - self.print_centaur_features(flags, disp); - } + #[cfg(not(dos_real))] + if is_centaur() + && let Some(centaur_str) = self.format_centaur_features(flags) + { + features.insert("Centaur", centaur_str); + } - disp.newline(); + if !features.is_empty() { + let keys = [ + "Base", "SSE", "AVX", "AVX512", "Security", "Math", "Other", "Centaur", "Cyrix", + ]; + disp.display_features(&features, &keys); } } } 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; @@ -354,7 +260,7 @@ impl TCpuDisplay for Cpu { } } - #[cfg(dos)] + #[cfg(dos_real)] { use super::is_cyrix; @@ -406,7 +312,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(); @@ -434,31 +340,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))] + #[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 @@ -468,14 +363,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); @@ -484,8 +375,8 @@ impl TCpuDisplay for Cpu { self.print_topology(flags, &disp); // Cache - #[cfg(not(dos))] - if self.cores.is_empty() { + #[cfg(not(dos_real))] + if !self.is_hybrid() { let cache_count = |share_count: u32| -> String { CpuDisplay::x86_cache_count( share_count, @@ -495,8 +386,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 +409,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(flags, &disp); + } // CPU Signature self.print_signature(flags, &disp); @@ -540,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(any(dos, dos32a)))] - println!(); + disp.newline(); } } } 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..419af050 100644 --- a/src/x86/dos/mod.rs +++ b/src/x86/dos/mod.rs @@ -1,8 +1,9 @@ -#![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 core::arch::asm; use core::fmt::Write; @@ -12,7 +13,7 @@ pub use allocator::init_heap; pub mod args; pub use args::*; -#[cfg(dos)] +#[cfg(dos_real)] pub mod cache; pub mod fallback; @@ -22,28 +23,61 @@ 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(); + let total_cores = mp_table.total_cores(); + let total_threads = mp_table.total_threads(); + + if mp_sockets > 1 || total_threads > cpu.topology.threads.count { + let sockets = TopologyTier::new(mp_sockets, DataSource::MpTable); + 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.topology.threads = TopologyTier::new( + threads, + DataSource::Calculated("MP Table logical processors"), + ); + 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.topology.speed.base == 0 { + let s = Speed::detect(); + if s.base > 0 { + cpu.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))] #[cold] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { - use crate::println; - - #[cfg(dos32a)] + #[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)] - println!("Panic!"); exit(1); } @@ -82,22 +116,256 @@ 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(dos32a)] + #[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)] + #[cfg(dos_real)] { for &b in s.as_bytes() { printc(b); @@ -116,7 +384,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 +399,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 +432,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 +453,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 +473,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 b6c15249..cea12aee 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 @@ -7,21 +7,43 @@ /// 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 processor sockets. + /// 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 detected socket count based on total logical processors and CPUID threads per package. #[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) + } + + /// 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) } } @@ -54,26 +76,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); @@ -83,22 +105,16 @@ 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 }; - - // MP Table lookup is only applicable to certain CPUs - if !(crate::x86::is_intel() || crate::x86::is_vortex() || crate::x86::is_centaur()) { - return table; - } + let mut table = MpTable { processors: 1 }; - // 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) { - 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 +142,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; 1024]; + 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 +182,11 @@ impl MpTable { } } - if sockets > 0 { Some(sockets) } else { None } + if processors > 0 { + Some(processors) + } else { + None + } } #[inline(never)] @@ -203,7 +243,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 @@ -238,3 +278,119 @@ 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); + } + + #[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/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 633f5808..e7d2bd74 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; @@ -81,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) { @@ -109,9 +108,8 @@ 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() { - match &*vendor { + if Cpu::easter_egg().is_some() { + 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); 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..563becd0 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,157 @@ 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.topology.sockets = efi_sockets; + let cores = cpu + .topology + .cores + .count + .max(cpuid_cores_per_package() * efi_sockets.count); + let threads = cpu + .topology + .threads + .count + .max(cpuid_threads_per_package() * efi_sockets.count); + cpu.topology.cores = + TopologyTier::new(cores, DataSource::Calculated("EFI sockets * CPUID cores")); + cpu.topology.threads = TopologyTier::new( + threads, + DataSource::Calculated("EFI 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. Frequency measurement (TSC stall or SMBIOS fallback) + if cpu.topology.speed.base == 0 { + let measured = Speed::detect(); + if measured.base > 0 { + cpu.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 4aa89475..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; @@ -45,5 +48,6 @@ pub use count::*; pub use cpu::*; pub use features::*; pub use fns::*; +pub use micro_arch::*; pub use quirks::*; 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..b1fb0ec8 100644 --- a/src/x86/topology.rs +++ b/src/x86/topology.rs @@ -1,21 +1,24 @@ 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}; +pub use crate::common::Topology; 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 +27,7 @@ impl Speed { let boost = res.ebx; if base == 0 { - return Speed::measure(); + return Speed::default(); } Speed { @@ -37,7 +40,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 +53,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 +131,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")] @@ -162,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. @@ -191,34 +191,14 @@ 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 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); @@ -252,37 +232,127 @@ impl Topology { threads, speed, cache, - domains, } } - /// 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_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(total_cores), + DataSource::Calculated("MP Table * CPUID cores"), + ); + topo.threads = TopologyTier::new( + 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); + } + } + 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 +366,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,12 +375,12 @@ 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")), ) } - fn detect_domains() -> DomainList { + pub(crate) fn detect_domains() -> DomainList { let d: DomainList = Vec::new(); if !is_valid_leaf(LEAF_0B) { 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..6a28c143 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,21 @@ impl Intel { } } - #[cfg(not(dos))] + #[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().is_some_and(|c| c.has_l3()); + + 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, s: CpuSignature, @@ -406,7 +420,8 @@ 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, _) => 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)), (0, 15, 0, 6, _) => brand_arch(MicroArch::CedarMill, "Cedar Mill/Tulsa", Some(N64)), @@ -602,7 +617,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 +626,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) { @@ -941,6 +956,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/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 diff --git a/tests/cpuid_dump_test.rs b/tests/cpuid_dump_test.rs index 9aa193c7..83a2c0e7 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; @@ -89,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( @@ -462,6 +473,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 +610,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 +1011,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.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.as_deref(), Some("Gracemont")); + } } ); @@ -1023,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 // ---------------------------------------------------------------------------- @@ -1068,3 +1145,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); +} 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);