Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions src/kernel/src/hal/arch/shared/cpu/interrupt/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
42 changes: 41 additions & 1 deletion src/libs/arch/src/x86/cpu/cpuid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 4 additions & 29 deletions src/uservm/src/vmm/microvm/whp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -728,14 +728,16 @@ 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;
},
_ => {},
}
}

// 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<u16> = match self
.inner
Expand Down Expand Up @@ -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.
///
Expand Down
75 changes: 75 additions & 0 deletions src/uservm/src/vmm/microvm/whp/partition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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::<WHV_X64_CPUID_RESULT>())
.map_err(|_| anyhow::anyhow!("CPUID result size overflow"))?;
WHvSetPartitionProperty(
handle,
WHvPartitionPropertyCodeCpuidResultList,
(&cpuid_entry as *const WHV_X64_CPUID_RESULT).cast::<std::ffi::c_void>(),
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| {
Expand All @@ -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::<WHV_CAPABILITY>()) {
Ok(s) => s,
Err(_) => return 0,
};
let result = unsafe {
WHvGetCapability(
WHvCapabilityCodeProcessorClockFrequency,
(&mut cap as *mut WHV_CAPABILITY).cast::<std::ffi::c_void>(),
cap_size,
None,
)
};
match result {
Ok(()) => unsafe { cap.ProcessorClockFrequency },
Err(e) => {
warn!("WHvGetCapability(ProcessorClockFrequency) failed: {e:?}");
0
}
}
}
}

impl Drop for WhpPartition {
Expand Down
Loading