diff --git a/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs b/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs index baff88944e..92c0a759e9 100644 --- a/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs +++ b/src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs @@ -147,6 +147,168 @@ impl InterruptController { }); } + // On microvm/WHP the partition enables LAPIC emulation in + // xAPIC mode. Enable the LAPIC software-enable bit and + // configure the LAPIC periodic timer so timer interrupts + // fire entirely inside the WHP LAPIC emulator — zero VM + // exits for timer delivery. The LAPIC page at 0xFEE00000 + // is identity-mapped via the microvm platform init and + // handled by the WHP LAPIC emulator (not guest RAM). + #[cfg(all(feature = "microvm", feature = "whp"))] + { + use ::arch::cpu::xapic; + let lapic_base: usize = ::config::microvm::DEFAULT_LAPIC_BASE; + let lapic: xapic::Xapic = xapic::Xapic::new(lapic_base as *mut u32); + // SAFETY: The LAPIC MMIO page is identity-mapped during + // microvm platform init. Writes go through the WHP LAPIC + // emulator. + unsafe { + lapic.write(xapic::XAPIC_SVR, 0x1FF); + lapic.write(xapic::XAPIC_TPR, 0); + } + info!("lapic svr enabled for whp interrupt delivery"); + + // LAPIC timer calibration. + // + // When CPUID leaf 0x16 is available, an RDTSC-based spin + // loop is used. This eliminates ~100 PIT-polling VM exits + // that are extremely expensive during the first + // WHvRunVirtualProcessor call (WHP lazily initialises + // internal partition state). + // + // When leaf 0x16 is not available, we fall back to + // PIT-based calibration with a reduced 1 ms window. + + // SAFETY: LAPIC registers go through the WHP emulator. + // CPUID and RDTSC do not cause VM exits. + unsafe { + // 1. Mask the LAPIC timer during calibration. + lapic.write( + xapic::XAPIC_TIMER, + xapic::XapicTimer::new(0x20, false, true, 0).to_u32(), + ); + + // 2. Set LAPIC timer divide-by-128. + lapic.write(xapic::XAPIC_TDCR, 0x0A); + + // 3. Check CPUID leaf 0x16 for TSC frequency. + let base_freq: u32 = ::arch::cpu::cpuid::get_base_frequency_mhz(); + + let mut ticks_per_ms: u32 = if base_freq > 0 { + // RDTSC-based calibration (zero VM exits). + let tsc_freq_mhz: u64 = base_freq as u64; + let tsc_ticks_per_ms: u64 = tsc_freq_mhz * 1_000; + + // 4a. Start the LAPIC timer counting from max value. + lapic.write(xapic::XAPIC_TICR, 0xFFFF_FFFF); + + // 5a. Spin for ~1 ms using RDTSC (zero VM exits). + // A max-iteration guard prevents a hang if TSC + // does not advance (virtualisation quirk). + const RDTSC_MAX_ITERS: u64 = 1_000_000_000; + let tsc_start: u64 = ::arch::cpu::rdtsc(); + let mut iters: u64 = 0; + while (::arch::cpu::rdtsc() - tsc_start) < tsc_ticks_per_ms { + core::hint::spin_loop(); + iters += 1; + if iters >= RDTSC_MAX_ITERS { + warn!( + "rdtsc calibration timeout after {} iterations", + RDTSC_MAX_ITERS + ); + break; + } + } + + // 6a. Read remaining LAPIC count and actual TSC + // delta to correct for overshoot. + let current_count: u32 = lapic.read(xapic::XAPIC_TCCR); + let elapsed_ticks: u32 = 0xFFFF_FFFF_u32.wrapping_sub(current_count); + let tsc_elapsed: u64 = ::arch::cpu::rdtsc() - tsc_start; + + // ticks_per_ms = elapsed × (target / actual) so the + // result is independent of TSC frequency errors. + let tpm: u32 = + ((elapsed_ticks as u64 * tsc_ticks_per_ms) / tsc_elapsed) as u32; + + info!( + "lapic timer calibration (rdtsc): elapsed_ticks={}, ticks_per_ms={}, \ + tsc_freq_mhz={}", + elapsed_ticks, tpm, tsc_freq_mhz + ); + tpm + } else { + // PIT-based fallback (reduced 1 ms window). + use ::arch::cpu::pit; + const CALIBRATION_MS: u32 = 1; + let pit_reload: u16 = + ((pit::PIT_MAX_FREQUENCY as u64 * CALIBRATION_MS as u64 / 1000) + & 0xFFFF) as u16; + + warn!("cpuid leaf 0x16 unavailable, using pit-based calibration fallback"); + + // 4b. Program PIT channel 2 in one-shot mode. + let speaker: u8 = (::arch::io::in8(0x61) & 0xFC) | 0x01; + ::arch::io::out8(0x61, speaker); + ::arch::io::out8( + pit::PIT_CTRL, + pit::PIT_SEL2 + | pit::PIT_ACC_LOHI + | pit::PIT_MODE_TCOUNT + | pit::PIT_BINARY, + ); + ::arch::io::out8(pit::PIT_DATA + 2, (pit_reload & 0xFF) as u8); + ::arch::io::out8(pit::PIT_DATA + 2, (pit_reload >> 8) as u8); + + // Start the LAPIC timer counting from max value. + lapic.write(xapic::XAPIC_TICR, 0xFFFF_FFFF); + + // 5b. Wait for PIT channel 2 output (bit 5 of + // port 0x61) with a bounded busy-wait. + const PIT_CALIBRATION_MAX_ITERS: u32 = 10_000_000; + let mut pit_iters: u32 = 0; + while (::arch::io::in8(0x61) & 0x20) == 0 { + core::hint::spin_loop(); + pit_iters = pit_iters.wrapping_add(1); + if pit_iters >= PIT_CALIBRATION_MAX_ITERS { + warn!( + "pit calibration timeout after {} iterations", + PIT_CALIBRATION_MAX_ITERS + ); + break; + } + } + + // 6b. Read remaining LAPIC timer count. + let current_count: u32 = lapic.read(xapic::XAPIC_TCCR); + let elapsed_ticks: u32 = 0xFFFF_FFFF_u32.wrapping_sub(current_count); + let tpm: u32 = elapsed_ticks / CALIBRATION_MS; + + info!( + "lapic timer calibration (pit fallback): elapsed_ticks={}, \ + ticks_per_ms={}", + elapsed_ticks, tpm + ); + tpm + }; + + if ticks_per_ms == 0 { + warn!("lapic timer calibration underflow: using fallback ticks_per_ms=1"); + ticks_per_ms = 1; + } + + // 7. Program LAPIC timer in periodic mode with vector + // 0x20, initial count = ticks_per_ms (1 kHz). + lapic.write( + xapic::XAPIC_TIMER, + xapic::XapicTimer::new(0x20, false, false, 1).to_u32(), + ); + lapic.write(xapic::XAPIC_TICR, ticks_per_ms); + + info!("lapic periodic timer started (vector=0x20, period=1ms)"); + } + } + info!("using legacy pic"); return Ok(Self { intmap, diff --git a/src/libs/arch/src/x86/cpu/cpuid.rs b/src/libs/arch/src/x86/cpu/cpuid.rs index 0ce1bfa10c..68f3ad97a9 100644 --- a/src/libs/arch/src/x86/cpu/cpuid.rs +++ b/src/libs/arch/src/x86/cpu/cpuid.rs @@ -758,13 +758,53 @@ pub fn has_pbe() -> bool { /// The processor base frequency in MHz, or 0 if not supported. /// pub fn get_base_frequency_mhz() -> u32 { + // Check the maximum supported basic CPUID leaf. let (max_basic_leaf, _, _, _): (u32, u32, u32, u32) = cpuid(0); if max_basic_leaf < CPUID_FREQUENCY { + // Leaf 0x16 is not supported. return 0; } - let (eax, _, _, _): (u32, u32, u32, u32) = cpuid_subleaf(CPUID_FREQUENCY, 0); + // Issue CPUID with EAX = CPUID_FREQUENCY and ECX = 0 explicitly, so the + // subleaf selector is well-defined and does not depend on caller state. + let mut eax: u32 = CPUID_FREQUENCY; + let ebx: u32; + let mut ecx: u32 = 0; + let edx: u32; + + unsafe { + #[cfg(target_pointer_width = "32")] + ::core::arch::asm!( + "mov {ebx_backup}, ebx", + "cpuid", + "mov {ebx_out}, ebx", + "mov ebx, {ebx_backup}", + ebx_backup = out(reg) _, + ebx_out = out(reg) ebx, + inout("eax") eax, + inout("ecx") ecx, + out("edx") edx, + options(nomem, preserves_flags, nostack) + ); + + #[cfg(target_pointer_width = "64")] + ::core::arch::asm!( + "mov {ebx_backup}, rbx", + "cpuid", + "mov {ebx_out:e}, ebx", + "mov rbx, {ebx_backup}", + ebx_backup = out(reg) _, + ebx_out = out(reg) ebx, + inout("eax") eax, + inout("ecx") ecx, + out("edx") edx, + options(nomem, preserves_flags, nostack) + ); + } + + // Suppress unused-variable warnings for registers we must clobber. + let _ = (ebx, ecx, edx); // EAX contains the processor base frequency in MHz. eax diff --git a/src/uservm/src/vmm/microvm/whp/mod.rs b/src/uservm/src/vmm/microvm/whp/mod.rs index 43614346a3..501042fb07 100644 --- a/src/uservm/src/vmm/microvm/whp/mod.rs +++ b/src/uservm/src/vmm/microvm/whp/mod.rs @@ -712,7 +712,7 @@ impl Vmm { // PIT channel 2 output: return gate/OUT2 status. 0x61 => { let value: u8 = pit_ch2.read_speaker(); - Self::set_guest_rax(self.partition_handle, value as u64); + self.vcpu.blocking_lock().set_rip_and_rax(value as u64); continue; }, // Other legacy ports: return zero. @@ -728,7 +728,7 @@ impl Vmm { | 0x3F8..=0x3FF | 0xCF8 | 0xCFC..=0xCFF => { - Self::set_guest_rax(self.partition_handle, 0); + self.vcpu.blocking_lock().set_rip_and_rax(0); continue; }, _ => {}, @@ -736,6 +736,8 @@ impl Vmm { } // Slow path: application-level I/O (stdout, stdin, VMM port). + // Flush any deferred RIP advance for reads that reach the slow path. + self.vcpu.blocking_lock().flush_pending_rip(); let exit_status: Option = match self .inner @@ -1155,33 +1157,6 @@ impl Vmm { } } - /// Sets the guest vCPU's RAX register. Used to return data for - /// emulated PmioIn instructions (RIP is already advanced by the - /// vCPU exit handler). - fn set_guest_rax( - partition: windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE, - value: u64, - ) { - use windows::Win32::System::Hypervisor::{ - WHV_REGISTER_NAME, - WHV_REGISTER_VALUE, - WHvSetVirtualProcessorRegisters, - }; - const WHV_X64_REGISTER_RAX: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0); - let reg_names: [WHV_REGISTER_NAME; 1] = [WHV_X64_REGISTER_RAX]; - let mut reg_values: [WHV_REGISTER_VALUE; 1] = [unsafe { std::mem::zeroed() }]; - reg_values[0].Reg64 = value; - unsafe { - let _ = WHvSetVirtualProcessorRegisters( - partition, - 0, - reg_names.as_ptr(), - 1, - reg_values.as_ptr(), - ); - } - } - /// Populates the pvclock shared memory page so the kernel reads accurate /// wall-clock time without relying on TSC. /// diff --git a/src/uservm/src/vmm/microvm/whp/partition.rs b/src/uservm/src/vmm/microvm/whp/partition.rs index e9e2cd83e4..8ed755bcb4 100644 --- a/src/uservm/src/vmm/microvm/whp/partition.rs +++ b/src/uservm/src/vmm/microvm/whp/partition.rs @@ -8,13 +8,20 @@ use ::anyhow::Result; use ::log::{ error, + info, trace, + warn, }; use windows::Win32::System::Hypervisor::{ + WHV_CAPABILITY, WHV_PARTITION_HANDLE, WHV_PARTITION_PROPERTY, + WHV_X64_CPUID_RESULT, + WHvCapabilityCodeProcessorClockFrequency, WHvCreatePartition, WHvDeletePartition, + WHvGetCapability, + WHvPartitionPropertyCodeCpuidResultList, WHvPartitionPropertyCodeLocalApicEmulationMode, WHvPartitionPropertyCodeProcessorCount, WHvSetPartitionProperty, @@ -107,6 +114,49 @@ impl WhpPartition { })?; } + // Override CPUID leaf 0x16 (Processor Frequency Information) so + // the guest kernel can use RDTSC-based LAPIC timer calibration + // instead of the PIT busy-wait loop (~908 VM exits eliminated). + // Hyper-V zeros out leaf 0x16 even when the host CPU supports it. + unsafe { + let freq_hz: u64 = Self::query_processor_clock_frequency(); + let freq_mhz: u32 = (freq_hz / 1_000_000) as u32; + if freq_mhz > 0 { + let cpuid_entry = WHV_X64_CPUID_RESULT { + Function: 0x16, + Reserved: [0; 3], + Eax: freq_mhz, + Ebx: freq_mhz, + Ecx: 0, + Edx: 0, + }; + let entry_size: u32 = + u32::try_from(std::mem::size_of::()) + .map_err(|_| anyhow::anyhow!("CPUID result size overflow"))?; + WHvSetPartitionProperty( + handle, + WHvPartitionPropertyCodeCpuidResultList, + (&cpuid_entry as *const WHV_X64_CPUID_RESULT).cast::(), + entry_size, + ) + .map_err(|e| { + let reason: String = + format!("failed to set CPUID result list (error={e:?})"); + error!("WhpPartition::new(): {reason}"); + anyhow::anyhow!(reason) + })?; + info!( + "overriding CPUID leaf 0x16: base_freq={}MHz (from {}Hz)", + freq_mhz, freq_hz + ); + } else { + warn!( + "could not query processor clock frequency; \ + guest will use PIT-based calibration" + ); + } + } + // Setup the partition (finalizes configuration). unsafe { WHvSetupPartition(handle).map_err(|e| { @@ -127,6 +177,31 @@ impl WhpPartition { pub fn handle(&self) -> WHV_PARTITION_HANDLE { self.handle } + + /// Queries the host processor TSC clock frequency via WHP. + /// Returns 0 if the query fails or the frequency is unavailable. + unsafe fn query_processor_clock_frequency() -> u64 { + let mut cap: WHV_CAPABILITY = unsafe { std::mem::zeroed() }; + let cap_size: u32 = match u32::try_from(std::mem::size_of::()) { + Ok(s) => s, + Err(_) => return 0, + }; + let result = unsafe { + WHvGetCapability( + WHvCapabilityCodeProcessorClockFrequency, + (&mut cap as *mut WHV_CAPABILITY).cast::(), + cap_size, + None, + ) + }; + match result { + Ok(()) => unsafe { cap.ProcessorClockFrequency }, + Err(e) => { + warn!("WHvGetCapability(ProcessorClockFrequency) failed: {e:?}"); + 0 + } + } + } } impl Drop for WhpPartition { diff --git a/src/uservm/src/vmm/microvm/whp/vcpu/mod.rs b/src/uservm/src/vmm/microvm/whp/vcpu/mod.rs index b9e659074c..9f1c37041d 100644 --- a/src/uservm/src/vmm/microvm/whp/vcpu/mod.rs +++ b/src/uservm/src/vmm/microvm/whp/vcpu/mod.rs @@ -169,6 +169,8 @@ pub struct VirtualProcessor { online: bool, /// Exit status code. exit_status: u16, + /// Deferred RIP value for PMIO reads (batched with RAX write). + pending_new_rip: Option, } // SAFETY: `VirtualProcessor` only stores a `WHV_PARTITION_HANDLE` (an opaque OS handle) and @@ -249,6 +251,7 @@ impl VirtualProcessor { index, online: false, exit_status: 0, + pending_new_rip: None, }) } @@ -633,7 +636,11 @@ impl VirtualProcessor { VirtualProcessorExitContext::Pmio(exit::PmioAccess::PmioOut(port, value, width)) } else { let data: Vec = vec![0u8; access_size as usize]; - self.advance_rip(exit_context); + // Defer RIP advance for reads — batched with RAX write. + let instruction_length: u64 = + u64::from(exit_context.VpContext._bitfield & 0xF); + self.pending_new_rip = + Some(exit_context.VpContext.Rip + instruction_length); VirtualProcessorExitContext::Pmio(exit::PmioAccess::PmioIn(port, data)) } }, @@ -675,6 +682,53 @@ impl VirtualProcessor { } } } + + /// Sets RAX and flushes any deferred RIP advance in a single WHvSet call. + pub fn set_rip_and_rax(&mut self, rax_value: u64) { + if let Some(new_rip) = self.pending_new_rip.take() { + let reg_names: [WHV_REGISTER_NAME; 2] = + [WHV_X64_REGISTER_RIP, WHV_X64_REGISTER_RAX]; + let mut reg_values: [WHV_REGISTER_VALUE; 2] = + [unsafe { mem::zeroed() }, unsafe { mem::zeroed() }]; + reg_values[0].Reg64 = new_rip; + reg_values[1].Reg64 = rax_value; + unsafe { + if let Err(e) = + whp_set_registers(self.partition, self.index, ®_names, ®_values) + { + warn!("set_rip_and_rax(): failed (error={e:?})"); + } + } + } else { + // No pending RIP — just set RAX. + let reg_names: [WHV_REGISTER_NAME; 1] = [WHV_X64_REGISTER_RAX]; + let mut reg_values: [WHV_REGISTER_VALUE; 1] = [unsafe { mem::zeroed() }]; + reg_values[0].Reg64 = rax_value; + unsafe { + if let Err(e) = + whp_set_registers(self.partition, self.index, ®_names, ®_values) + { + warn!("set_rip_and_rax(): failed to set RAX (error={e:?})"); + } + } + } + } + + /// Flushes any deferred RIP advance (for exits handled without setting RAX). + pub fn flush_pending_rip(&mut self) { + if let Some(new_rip) = self.pending_new_rip.take() { + let reg_names: [WHV_REGISTER_NAME; 1] = [WHV_X64_REGISTER_RIP]; + let mut reg_values: [WHV_REGISTER_VALUE; 1] = [unsafe { mem::zeroed() }]; + reg_values[0].Reg64 = new_rip; + unsafe { + if let Err(e) = + whp_set_registers(self.partition, self.index, ®_names, ®_values) + { + warn!("flush_pending_rip(): failed (error={e:?})"); + } + } + } + } } impl Drop for VirtualProcessor {