diff --git a/Cargo.lock b/Cargo.lock index f93dcea121d..f94fb26db5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,6 +1634,7 @@ dependencies = [ "toyos-rootimage", "toyos-symbols", "toyos-tco", + "toyos-tsc", "toyos-untrusted", "toyos-userbound", "toyos-wallclock", @@ -2651,6 +2652,10 @@ dependencies = [ "toyos-untrusted", ] +[[package]] +name = "toyos-tsc" +version = "0.1.0" + [[package]] name = "toyos-untrusted" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 58d9c164abe..86fb3420eef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ members = [ "toyos-symbols", "toyos-tco", "toyos-tmpdir", + "toyos-tsc", "toyos-untrusted", "toyos-update", "toyos-userbound", diff --git a/bootloader/Cargo.lock b/bootloader/Cargo.lock index e342c462790..ca2f5e69e3a 100644 --- a/bootloader/Cargo.lock +++ b/bootloader/Cargo.lock @@ -41,6 +41,7 @@ dependencies = [ "toyos-gpt", "toyos-rootimage", "toyos-tco", + "toyos-tsc", "toyos-update", "toyos-wallclock", "uefi", @@ -294,6 +295,10 @@ version = "0.1.0" name = "toyos-tco" version = "0.1.0" +[[package]] +name = "toyos-tsc" +version = "0.1.0" + [[package]] name = "toyos-update" version = "0.1.0" diff --git a/bootloader/Cargo.toml b/bootloader/Cargo.toml index 089250dbe72..336515d252b 100644 --- a/bootloader/Cargo.toml +++ b/bootloader/Cargo.toml @@ -27,6 +27,9 @@ toyos-gpt = { path = "../toyos-gpt" } toyos-rootimage = { path = "../toyos-rootimage" } bcachefs = { path = "../bcachefs", default-features = false } toyos-tco = { path = "../toyos-tco" } +# The counter's rate each line's stamp is converted at, decoded as the kernel +# decodes it. +toyos-tsc = { path = "../toyos-tsc" } # The signed image, the slot table and the anti-rollback floor: every decision # this loader makes about which slot to boot, host-tested there. toyos-update = { path = "../toyos-update" } diff --git a/bootloader/src/arch/aarch64.rs b/bootloader/src/arch/aarch64.rs index 67b935439b1..94106d6dfb1 100644 --- a/bootloader/src/arch/aarch64.rs +++ b/bootloader/src/arch/aarch64.rs @@ -41,6 +41,15 @@ pub fn counter() -> u64 { count } +/// [`counter`]'s rate in hertz as firmware states it in `CNTFRQ_EL0`, or +/// `None` where it reads zero. +pub fn counter_hz() -> Option { + let hz: u64; + // SAFETY: reads a register EL1 and EL2 may always read. + unsafe { core::arch::asm!("mrs {}, cntfrq_el0", out(reg) hz, options(nomem, nostack, preserves_flags)) }; + (hz != 0).then_some(hz) +} + /// What the loader says about the CPU as firmware handed it over, or why the /// kernel cannot run on it: entered at EL2 on a CPU without FEAT_E2H0, /// `HCR_EL2.E2H` is RES1, so the kernel's entry cannot clear it and every diff --git a/bootloader/src/arch/x86_64.rs b/bootloader/src/arch/x86_64.rs index ccb1ada9b72..78e2e25c615 100644 --- a/bootloader/src/arch/x86_64.rs +++ b/bootloader/src/arch/x86_64.rs @@ -20,6 +20,19 @@ pub fn counter() -> u64 { unsafe { core::arch::x86_64::_rdtsc() } } +/// [`counter`]'s rate in hertz as CPUID states it, or `None` on a part that +/// states none: the kernel's `stated_counter_hz`, read before the kernel runs. +pub fn counter_hz() -> Option { + use core::arch::x86_64::__cpuid; + // A leaf past the maximum is never asked for, so none is misread as data. + let leaf = |at: u32| { + let r = __cpuid(at); + (r.eax, r.ebx, r.ecx, r.edx) + }; + let max = leaf(0).0; + toyos_tsc::stated_hz((max >= 0x15).then(|| leaf(0x15)), (max >= 0x16).then(|| leaf(0x16))) +} + /// `CLFLUSH`'s line on every x86-64 part. const LINE: u64 = 64; diff --git a/bootloader/src/loaderlog.rs b/bootloader/src/loaderlog.rs index 615566ca9b0..175fb03ead4 100644 --- a/bootloader/src/loaderlog.rs +++ b/bootloader/src/loaderlog.rs @@ -224,5 +224,5 @@ fn unique_guid(bs: &BootServices, handle: Handle) -> Option<[u8; 16]> { /// Why there is no log, and that the boot goes on without one. fn refused(why: fmt::Arguments) { // The console directly: there is no file, and this says why. - uefi_services::println!("Loader log: {why}. This boot's loader lines are on the screen only"); + uefi_services::println!("{}Loader log: {why}. This boot's loader lines are on the screen only", crate::stamp::now()); } diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index f667d65e736..e800acfdf55 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -25,16 +25,11 @@ use toyos_update::policy; use toyos_update::record::{self, Booted, Ended, Record}; /// Every line this loader prints: the firmware's console, and the file on the -/// stick once [`loaderlog::open`] has one. The arguments are evaluated once, so -/// a line that reads a clock says the same on both. +/// stick once [`loaderlog::open`] has one, under one [`stamp`]. The arguments +/// are evaluated once, so a line that reads a clock says the same on both. macro_rules! println { ($($arg:tt)*) => { - match core::format_args!($($arg)*) { - args => { - uefi_services::println!("{}", args); - $crate::loaderlog::line(args); - } - } + $crate::stamp::say(core::format_args!($($arg)*)) }; } @@ -49,6 +44,7 @@ mod loaderlog; mod rootbridge; mod rootimage; mod slot; +mod stamp; mod watchdog; /// The largest file the bootloader will read off the ESP. @@ -747,6 +743,7 @@ fn end_this_pass(system_table: &SystemTable, exit_event: Option) -> fn main(handle: Handle, mut system_table: SystemTable) -> Status { // First, so this reading is firmware's time and none of the loader's. let entry_counter = arch::counter(); + let counter_hz = stamp::start(entry_counter); let exit_event = uefi_services::init(&mut system_table).unwrap(); // First, because it covers everything below it: firmware starts a // five-minute countdown when it loads an image and resets the machine if @@ -755,6 +752,11 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // so the answer is on the stick and not only on the screen. let firmware_watchdog = system_table.boot_services().set_watchdog_timer(FIRMWARE_WATCHDOG_SECS, WATCHDOG_CODE, None); + // Before the first line, so the screen holds this loader's lines and none + // of the firmware's: its logo and its boot manager's text. ClearScreen + // (UEFI 2.11 §12.4.8) is the console's own clear, which also homes the + // cursor; said once the log is open. + let cleared = system_table.stdout().clear(); // The same sixteen bytes the kernel is handed below, read once, and read // before the first line so that no line is only on the screen. let log_guid = log_partition_guid(handle, &system_table); @@ -810,6 +812,13 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { let wrote = attempt::write(&system_table, &log_guid, &record); loaderlog::open(&system_table, &log_guid, finding.is_none() && !retry); println!("{}", loaderlog::BEGINS_AT); + match counter_hz { + Some(hz) => println!("Loader clock: each line opens with the ms since this loader's entry, at the counter's stated {hz} Hz"), + None => println!("Loader clock: this CPU states no counter rate, so no line carries the time it was said"), + } + if let Err(e) = cleared { + println!("Screen: firmware's console would not clear ({e}), so its own lines stay above these"); + } if let Some(line) = claim_refused { println!("{line}"); } @@ -864,7 +873,7 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { // firmware's own scroll costs this machine, and every reader of these // lines reads them off the stick. for line in &finding.filed { - loaderlog::line(format_args!("{line}")); + loaderlog::line(format_args!("{}{line}", stamp::now())); } if finding.ends_the_chain { // The last boot is accounted for, so this pass boots no kernel. diff --git a/bootloader/src/stamp.rs b/bootloader/src/stamp.rs new file mode 100644 index 00000000000..de5695259d9 --- /dev/null +++ b/bootloader/src/stamp.rs @@ -0,0 +1,50 @@ +//! The head of every line the loader says: how far into the loader it was +//! said, in milliseconds since `main`'s first statement, by the CPU's counter +//! at the rate the CPU states ([`crate::arch::counter_hz`]). No wall clock: a +//! line is placed against the loader's own start and nothing else. +//! +//! A CPU that states no rate gets lines with no head, and [`start`]'s caller +//! says so once. + +use core::fmt; +use core::sync::atomic::{AtomicU64, Ordering}; + +/// The counter at the loader's entry, and its rate; zero until [`start`]. +static ENTRY: AtomicU64 = AtomicU64::new(0); +static HZ: AtomicU64 = AtomicU64::new(0); + +/// Take `entry` as every stamp's zero, and return the rate stamps are +/// converted at, or `None` where the CPU states none. +pub fn start(entry: u64) -> Option { + let hz = crate::arch::counter_hz(); + ENTRY.store(entry, Ordering::Relaxed); + HZ.store(hz.unwrap_or(0), Ordering::Relaxed); + hz +} + +/// Now, as a line's head. +pub fn now() -> Stamp { + let hz = HZ.load(Ordering::Relaxed); + let ticks = crate::arch::counter() + .checked_sub(ENTRY.load(Ordering::Relaxed)) + .expect("the counter ran backwards since the loader's entry"); + Stamp((hz != 0).then(|| (u128::from(ticks) * 1_000 / u128::from(hz)) as u64)) +} + +/// One line, stamped, to the console and then to `loader.log`. +pub fn say(args: fmt::Arguments) { + let stamp = now(); + uefi_services::println!("{stamp}{args}"); + crate::loaderlog::line(format_args!("{stamp}{args}")); +} + +pub struct Stamp(Option); + +impl fmt::Display for Stamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(ms) => write!(f, "{ms:>5} ms "), + None => Ok(()), + } + } +} diff --git a/issues/every-loader-line-costs-the-t14-a-fixed-13-ms.md b/issues/every-loader-line-costs-the-t14-a-fixed-13-ms.md new file mode 100644 index 00000000000..6a7097df7a1 --- /dev/null +++ b/issues/every-loader-line-costs-the-t14-a-fixed-13-ms.md @@ -0,0 +1,33 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# Every loader line costs the T14 a fixed 13 ms, whatever its length + +The T14's `loader.log` from a `jobcase` boot at `adb0a86c3` (each line stamped +with its ms since the loader's entry, at the 2419200000 Hz CPUID 15H states, +and the stamps agreeing with the kernel's HPET-calibrated `loader 1877 ms`) +spaces consecutive lines 12-14 ms apart wherever the loader does no other work +between them, and the length of the line does not move it: `Kernel: 3225456 +bytes` (21 characters) follows its predecessor by 12 ms, each ~150-character +`Loading segment:` line by 13-14 ms. + +Each line is two writes: ConOut's `OutputString`, and `loaderlog::line`'s +`file.write` and `file.flush()` to `loader.log` on the stick's FAT +(`bootloader/src/loaderlog.rs`). The file is the cost. A `Loader lines:` line that +summed each half's counter ticks, carried at `d373045ee` for this one reading +and removed after it, read on the T14's `jobcase` boot at that commit: the +43 lines above it took 129796443 counter ticks on the console (53.7 ms, 1.2 ms +a line) and 1505476678 in `loader.log` (622.3 ms, 14.5 ms a line), and the +screen's clear before the first line 250073763 (103.4 ms), at 2419200000 Hz. + +The fix is fewer flushes, and that gives up `loaderlog`'s claim that a line is +written and flushed before the loader goes on: what a hang between two flushes +would lose is a decision of its own, not a side effect of the fix. + +The stamps' spacing reads the fix without it. + +Exit: the T14's loader lines cost what their content costs to draw and keep, +with the durability `loader.log` keeps stated at `loaderlog`. diff --git a/issues/the-loaders-lines-carry-no-time-on-an-x86-cpu-that-states-no-tsc-rate.md b/issues/the-loaders-lines-carry-no-time-on-an-x86-cpu-that-states-no-tsc-rate.md new file mode 100644 index 00000000000..f360d617de4 --- /dev/null +++ b/issues/the-loaders-lines-carry-no-time-on-an-x86-cpu-that-states-no-tsc-rate.md @@ -0,0 +1,33 @@ +--- +status: open +kind: defect +opened: 2026-10-04 +--- + +# The loader's lines carry no time on an x86 CPU that states no TSC rate + +`bootloader/src/stamp.rs` converts the cycle counter at the rate CPUID leaves +15H/16H state (`toyos_tsc::stated_hz`), and a CPU that states neither gets +unstamped lines and one `Loader clock: this CPU states no counter rate` line. + +Every AMD part is one. InstLatx64's recorded CPUID dumps +(`github.com/InstLatx64/InstLatx64`, `AuthenticAMD/`) read leaf 0's EAX, the +largest standard leaf, as `00000010` on Zen 2 (`0870F10_K17_Matisse_CPUID`), +Zen 3+ (`0A40F41_K19_Rembrandt_01_CPUID`), Zen 4 (`0A60F12_K19_Raphael_01_CPUID`) +and Zen 5 (`0B00F21_K20_Turin_01_CPUID`, `0B20F40_K20_StrixPoint_06_CPUID`, +`0B40F40_K20_GraniteRidge_02_CPUID`): neither leaf exists there. QEMU's +`qemu64` states neither either, and under the `guest / suite` check's +`-cpu host` whether a line is stamped is the runner's CPU's to decide. + +A second rate source is the fix, and the choice of one is open: the +`EFI_TIMESTAMP_PROTOCOL`'s stated frequency, the ACPI PM timer or the HPET, each +a calibration the loader then pays for before its first stamped line. + +Until then no guest test sees the stamp on the screen: `screen_loader_clears` +(`tests/toyos.rs`) matches the screen's first line with or without its head, +because no x86 guest stamps one. The fix is where that test starts asserting +the head on the screen. + +Exit: a loader line carries its milliseconds on an x86-64 CPU that states +neither leaf 15H's ratio nor leaf 16H's base frequency, and `screen_loader_clears` +asserts that line's head on the screen. diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 20a78b0a455..a4ad75c55ed 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -56,6 +56,7 @@ dependencies = [ "toyos-rootimage", "toyos-symbols", "toyos-tco", + "toyos-tsc", "toyos-untrusted", "toyos-userbound", "toyos-wallclock", @@ -168,6 +169,10 @@ dependencies = [ name = "toyos-tco" version = "0.1.0" +[[package]] +name = "toyos-tsc" +version = "0.1.0" + [[package]] name = "toyos-untrusted" version = "0.1.0" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index ad2cc27b51c..35171680d6a 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -509,6 +509,7 @@ toyos-gpt = { path = "../toyos-gpt" } toyos-hda = { path = "../toyos-hda" } toyos-pci = { path = "pci" } toyos-tco = { path = "../toyos-tco" } +toyos-tsc = { path = "../toyos-tsc" } toyos-ps2 = { path = "ps2" } toyos-rootimage = { path = "../toyos-rootimage" } toyos-quiesce = { path = "../toyos-quiesce" } diff --git a/kernel/src/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index 0f514f69b12..eebe1400173 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -423,42 +423,12 @@ pub fn frame_pointer() -> u64 { /// and its caller says so rather than inventing a rate. pub fn stated_counter_hz() -> Option { let max_leaf = cpuid(0, 0).0; - tsc_hz_from( + toyos_tsc::stated_hz( (max_leaf >= 0x15).then(|| cpuid(0x15, 0)), (max_leaf >= 0x16).then(|| cpuid(0x16, 0)), ) } -/// SDM Vol. 2A, CPUID leaf 15H: EAX is the denominator and EBX the numerator of -/// the core crystal's ratio to the TSC, ECX the crystal's hertz — any of the -/// three reading zero means the leaf states nothing. Leaf 16H's EAX is the -/// processor base frequency in MHz, which an invariant TSC counts at. -const fn tsc_hz_from( - leaf15: Option<(u32, u32, u32, u32)>, - leaf16: Option<(u32, u32, u32, u32)>, -) -> Option { - if let Some((denominator, numerator, crystal_hz, _)) = leaf15 { - if denominator != 0 && numerator != 0 && crystal_hz != 0 { - return Some(crystal_hz as u64 * numerator as u64 / denominator as u64); - } - } - if let Some((base_mhz, _, _, _)) = leaf16 { - if base_mhz != 0 { - return Some(base_mhz as u64 * 1_000_000); - } - } - None -} - -const _: () = { - // The ratio, then the fall-through to the base frequency when the crystal - // is not enumerated, then the CPU that states neither. - assert!(matches!(tsc_hz_from(Some((2, 4, 25_000_000, 0)), None), Some(50_000_000))); - assert!(matches!(tsc_hz_from(Some((0, 0, 0, 0)), Some((2_400, 0, 0, 0))), Some(2_400_000_000))); - assert!(tsc_hz_from(None, Some((0, 0, 0, 0))).is_none()); - assert!(tsc_hz_from(None, None).is_none()); -}; - /// The thread pointer this CPU is running with: the FS base, which user TLS is addressed from. #[inline] diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 334bfc3a071..d7c0ce1b2cc 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -1295,6 +1295,27 @@ impl QemuInstance { Ok(read) } + /// Reset the machine with its memory kept, and hold it at the next reset + /// the guest itself asks for: the boot that follows keeps its screen, + /// stopped, where a `-no-reboot` QEMU would have exited with it. The + /// machine is stopped while QEMU's reset actions change, so the guest has + /// no reset of its own to land between them. + pub fn reset_and_hold_the_next(&mut self, budget: Duration) -> Result<(), String> { + let socket = self.sockets.qmp.clone().expect("reset_and_hold_the_next needs BootOptions { qmp: true }"); + let mut qmp = Qmp::connect(&socket); + qmp.execute("{\"execute\":\"stop\"}"); + qmp.execute("{\"execute\":\"set-action\",\"arguments\":{\"reboot\":\"reset\"}}"); + qmp.execute("{\"execute\":\"system_reset\"}"); + qmp.execute("{\"execute\":\"set-action\",\"arguments\":{\"reboot\":\"shutdown\",\"shutdown\":\"pause\"}}"); + qmp.execute("{\"execute\":\"cont\"}"); + qmp.stream.set_read_timeout(Some(self.budget(budget))).expect("qmp: the hold's budget"); + match QmpShutdown(qmp).reason().as_deref() { + Some("guest-reset") => Ok(()), + Some(other) => Err(format!("the boot after the reset stopped by {other:?}, not by a reset of its own")), + None => Err(format!("the boot after the reset never asked for a reset within {budget:?}")), + } + } + /// Capture the guest's scanout through QMP and return the decoded PPM. /// /// After a halt the guest is stopped, so the dump is stable. QEMU writes diff --git a/tests/toyos.rs b/tests/toyos.rs index cbead411bd6..bca88e9f617 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -198,6 +198,9 @@ const SCREEN_TESTS: &[(&str, qemu::Profile)] = &[ // The same fatal path with a compositor holding the panel, which is the // only configuration the owner's laptop is ever in. ("screen_fatal_halt_composited", qemu::Profile::Metal), + // What the loader leaves on the panel: its own lines, and none of the + // firmware's. + ("screen_loader_clears", qemu::Profile::Metal), ("virt_early_panic", qemu::Profile::Virt), ("virt_early_fault", qemu::Profile::Virt), ("virt_el2_drop", qemu::Profile::VirtEl2NoVhe), @@ -2060,6 +2063,69 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re )?; Ok(()) } + "screen_loader_clears" => { + // A loader's screen lasts until the kernel's first paint, which no + // poll is sure to catch, so the machine is held where a loader pass + // ends in a reset of its own: the pass after a reset that kept + // memory reads the black box the first pass armed, and hands the + // machine back. + let mut qemu = QemuInstance::boot_with_options( + test_config, + &[], + &[], + BootOptions { profile, qmp: true, ready_marker: bootlog::LOADER_LAST_LINE, ..Default::default() }, + ); + qemu.reset_and_hold_the_next(Duration::from_secs(30))?; + let serial = qemu.drain_until(Duration::from_secs(5), |l| l.contains(bootlog::CHAIN_ENDS_LINE)); + if !serial.contains(bootlog::CHAIN_ENDS_LINE) { + return Err(format!("the held boot is not a loader pass that ended its chain:\n{serial}")); + } + let dump = qemu.screendump(); + const BLACK: [u8; 3] = [0, 0, 0]; + // The firmware's logo is in colours its console's text is not. + let colours: BTreeSet<[u8; 3]> = dump.pixels.iter().copied().collect(); + if colours.len() != 2 || !colours.contains(&BLACK) { + return Err(format!( + "the loader's screen carries {} colours, not black and the console's text alone: \ + something the firmware drew is still on it", + colours.len() + )); + } + // The top line of ink is the loader's first, as wide as its + // characters at the 8-pixel glyphs of EDK2's console: the firmware's + // boot manager announces the option it starts on a line of its own. + // A pass that reads a finding appends to its log, so its first line + // is the separator; what precedes it on the 16550 is the terminal's + // escapes for the clear. + let Some(first) = serial.lines().find(|l| l.contains(bootlog::SEPARATOR)) else { + return Err(format!("the held pass never wrote {:?}:\n{serial}", bootlog::SEPARATOR)); + }; + // An escape runs from ESC to its final letter. + let shown = match first.rfind('\x1b') { + Some(at) => { + let escape = &first[at..]; + &escape[escape.find(|c: char| c.is_ascii_alphabetic()).map_or(escape.len(), |end| end + 1)..] + } + None => first, + }; + // A stamp's leading spaces carry no ink. + let chars = shown.trim().len(); + let (width, pixels) = (dump.width, &dump.pixels); + let inked = |y: usize| (0..width).filter(move |&x| pixels[y * width + x] != BLACK); + let Some(top) = (0..dump.height).find(|&y| inked(y).next().is_some()) else { + return Err("the loader's screen is blank".to_string()); + }; + let bottom = (top..dump.height).find(|&y| inked(y).next().is_none()).unwrap_or(dump.height); + let xs: Vec = (top..bottom).flat_map(inked).collect(); + let wide = xs.iter().max().unwrap() - xs.iter().min().unwrap() + 1; + if wide <= 8 * (chars - 1) || wide > 8 * chars { + return Err(format!( + "the screen's top line is {wide} px wide, and the loader's first, {shown:?}, \ + is {chars} characters: something else is above the loader's lines\n{serial}" + )); + } + Ok(()) + } "virt_early_panic" => { // The AArch64 port's stage 3, whole: the loader on AAVMF, the entry's // drop and declaration, the PL011 SPCR names, the boot's survey of diff --git a/toyos-tsc/Cargo.toml b/toyos-tsc/Cargo.toml new file mode 100644 index 00000000000..23094e14759 --- /dev/null +++ b/toyos-tsc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "toyos-tsc" +description = "The rate x86-64's time-stamp counter counts at, as CPUID leaves 15H and 16H state it, pure." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[lints.rust] +unsafe_code = "forbid" diff --git a/toyos-tsc/src/lib.rs b/toyos-tsc/src/lib.rs new file mode 100644 index 00000000000..e597f923dad --- /dev/null +++ b/toyos-tsc/src/lib.rs @@ -0,0 +1,39 @@ +//! The time-stamp counter's rate as the part states it, read by the kernel and +//! the loader alike. Pure: the caller executes CPUID and hands over the +//! registers. + +#![no_std] + +/// The registers CPUID returns for one leaf: EAX, EBX, ECX, EDX. +pub type Leaf = (u32, u32, u32, u32); + +/// SDM Vol. 2A, CPUID leaf 15H: EAX is the denominator and EBX the numerator of +/// the core crystal's ratio to the TSC, ECX the crystal's hertz — any of the +/// three reading zero means the leaf states nothing. Leaf 16H's EAX is the +/// processor base frequency in MHz, which an invariant TSC counts at. `None` +/// for a leaf the CPU does not implement, and as the answer where neither +/// states a rate: nothing here guesses one. +pub const fn stated_hz(leaf15: Option, leaf16: Option) -> Option { + if let Some((denominator, numerator, crystal_hz, _)) = leaf15 + && denominator != 0 + && numerator != 0 + && crystal_hz != 0 + { + return Some(crystal_hz as u64 * numerator as u64 / denominator as u64); + } + if let Some((base_mhz, _, _, _)) = leaf16 + && base_mhz != 0 + { + return Some(base_mhz as u64 * 1_000_000); + } + None +} + +const _: () = { + // The ratio, then the fall-through to the base frequency when the crystal + // is not enumerated, then the CPU that states neither. + assert!(matches!(stated_hz(Some((2, 4, 25_000_000, 0)), None), Some(50_000_000))); + assert!(matches!(stated_hz(Some((0, 0, 0, 0)), Some((2_400, 0, 0, 0))), Some(2_400_000_000))); + assert!(stated_hz(None, Some((0, 0, 0, 0))).is_none()); + assert!(stated_hz(None, None).is_none()); +};