//! Local APIC and its timer — the source of device interrupts.
//!
//! Modern x86 routes interrupts through the per-CPU Local APIC (the legacy 8259
//! PIC is remapped out of the way and masked). The LAPIC also has a built-in
//! timer, which is the simplest device interrupt to bring up: it needs no
//! external routing, just a vector and a count. We use it as danos's heartbeat.
//!
//! The LAPIC is memory-mapped (default physical 0xFEE00000, inside our identity
//! map). Every interrupt must be acknowledged with an end-of-interrupt write, or
//! the LAPIC won't deliver the next one.

const boot_handoff = @import("boot-handoff");
const io = @import("io.zig");
const paging = @import("paging.zig");

/// The ACPI PM timer, as a calibration reference: an I/O port or MMIO counter.
pub const PmTimer = struct { mmio: bool, address: u64, is_32bit: bool };

// Platform facts from discovery (set by `configure` before bring-up). Defaults are
// the legacy-safe assumptions so the code still works if discovery never ran.
var configuration_pic_present: bool = true;
var configuration_hpet_base: u64 = 0; // 0 = no HPET discovered
var configuration_pm_timer: ?PmTimer = null;
/// Which reference the last calibration used, for logging.
var cal_source: []const u8 = "none";

/// Hand the LAPIC bring-up the discovered platform facts. Call before `init`.
pub fn configure(pic_present: bool, hpet_base: u64, pm_timer: ?PmTimer) void {
    configuration_pic_present = pic_present;
    configuration_hpet_base = hpet_base;
    configuration_pm_timer = pm_timer;
}

/// The calibration reference the timer was measured against ("cpuid"/"hpet"/…).
pub fn calibrationSource() []const u8 {
    return cal_source;
}

/// IDT vector the timer fires on (in the device range, >= 32).
pub const timer_vector = 32;
/// Spurious-interrupt vector. Low nibble 0xF by convention; also in our gate
/// range so a stray spurious interrupt lands on a valid (no-op) handler.
const spurious_vector = 47;

// LAPIC register offsets.
const register_spurious = 0x0F0;
const register_eoi = 0x0B0;
const register_id = 0x020; // this core's LAPIC id, in bits 24-31
const register_icr_low = 0x300; // interrupt command register, low dword (writing it sends)
const register_icr_high = 0x310; // ICR high dword (destination APIC id in bits 24-31)
const register_lvt_timer = 0x320;
const register_timer_initial = 0x380;
const register_timer_current = 0x390;
const register_timer_divide = 0x3E0;

const icr_delivery_pending = 1 << 12; // ICR low bit 12: a previous IPI is still in flight

const lvt_masked = 1 << 16;
const lvt_periodic = 1 << 17;
const timer_divide_16 = 0x3;

const ia32_apic_base_msr = 0x1B;

/// LAPIC MMIO base. A runtime var (not a constant) both because we read it from
/// the MSR and so register writes compile to normal stores rather than a
/// `mov moffs`, which the self-hosted backend can't encode.
var base: usize = 0xFEE00000;

var tick_count: u64 = 0;

/// LAPIC timer counts per millisecond, measured against the PIT (see calibrate).
/// At divide-by-16, this is the effective counting rate.
var ticks_per_ms: u32 = 0;
/// The periodic-interrupt frequency the timer is armed at, once initTimer runs.
var timer_hz: u32 = 0;

/// TSC (Time Stamp Counter) calibration: cycles per second, and the count at boot.
/// The TSC is a per-core cycle counter, giving a ~nanosecond high-resolution
/// monotonic clock — far finer than the millisecond timer tick.
var tsc_hz: u64 = 0;
var tsc_base: u64 = 0;

/// Whether the TSC is architecturally **invariant** — a constant rate regardless of
/// P/C-state transitions, and thus valid as a clocksource (CPUID leaf 0x80000007,
/// EDX bit 8). AMD and modern Intel set it; the bare qemu64 model does not. Measured
/// frequency alone is not enough: a non-invariant TSC speeds up and slows down with
/// the core clock, so reading it as wall time would drift.
var tsc_invariant: bool = false;
/// Cleared if the cross-core warp check (checkWarpSource) ever sees the TSC read
/// lower on one core than the max another core has already published — i.e. the
/// per-core TSCs are not synchronized, and a task migrating cores could see time go
/// backward. Starts true (assume synchronized until proven otherwise).
var tsc_synced: bool = true;
/// The worst backward skew the warp check observed, in TSC cycles (0 = none).
var tsc_warp_cycles: u64 = 0;

/// The monotonic clock's source. The TSC when it is invariant *and* synchronized —
/// the fast `rdtsc` path taken on real Intel/AMD and modern VMs. Otherwise the HPET
/// main counter: a single fixed-rate counter, immune to both per-core skew and
/// frequency scaling, so it stays accurate on a bare VM or a warped machine.
const ClockSource = enum { tsc, hpet };
var clock_source: ClockSource = .tsc;

/// HPET standby clocksource, set up in calibrate() whenever an HPET exists (whether
/// or not calibration itself measured against it): its frequency, the counter value
/// chosen as the zero point, and its width mask. Only a 64-bit HPET is used as a
/// clocksource — a 32-bit one wraps too fast to be monotonic without accumulation.
var hpet_clock_hz: u64 = 0;
var hpet_clock_base: u64 = 0;
var hpet_clock_mask: u64 = ~@as(u64, 0);

/// Read the 64-bit Time Stamp Counter.
fn rdtsc() u64 {
    var low: u32 = undefined;
    var high: u32 = undefined;
    asm volatile ("rdtsc"
        : [low] "={eax}" (low),
          [high] "={edx}" (high),
    );
    return (@as(u64, high) << 32) | low;
}

fn read(register: u32) u32 {
    return @as(*volatile u32, @ptrFromInt(base + register)).*;
}
fn write(register: u32, value: u32) void {
    @as(*volatile u32, @ptrFromInt(base + register)).* = value;
}

/// Move the legacy 8259 PIC's vectors to 0x20-0x2F (clear of the CPU exception
/// vectors) and mask every line, so it can't deliver interrupts behind the APIC.
fn remapAndMaskPic() void {
    io.outb(0x20, 0x11); // start init (cascade mode)
    io.outb(0xA0, 0x11);
    io.outb(0x21, 0x20); // master offset 0x20
    io.outb(0xA1, 0x28); // slave offset 0x28
    io.outb(0x21, 0x04); // tell master about slave on IRQ2
    io.outb(0xA1, 0x02);
    io.outb(0x21, 0x01); // 8086 mode
    io.outb(0xA1, 0x01);
    io.outb(0x21, 0xFF); // mask all
    io.outb(0xA1, 0xFF);
}

/// Enable the Local APIC: mask the PIC (only if one is present — a legacy-free
/// UEFI Class 3 machine may have none), set the global-enable MSR bit, and
/// software-enable the APIC via its spurious-vector register.
pub fn init() void {
    if (configuration_pic_present) remapAndMaskPic();

    const msr = io.rdmsr(ia32_apic_base_msr);
    // Reach the LAPIC through the physmap (paging.init maps its page there).
    base = @intCast(boot_handoff.physicalToVirtual(msr & 0xFFFFF000)); // physical base is bits 12+
    io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable

    write(register_spurious, 0x100 | spurious_vector); // bit 8 = software enable
}

/// Software-enable *this* core's Local APIC — the application-processor counterpart
/// of `init`, minus the one-time PIC remap (the BSP already masked it) and minus
/// calibration (the timer rate is a shared hardware constant, measured once). Each
/// core has its own LAPIC at the same MMIO address, so no per-core base is needed.
pub fn initSecondary() void {
    const msr = io.rdmsr(ia32_apic_base_msr);
    io.wrmsr(ia32_apic_base_msr, msr | (1 << 11)); // global enable
    write(register_spurious, 0x100 | spurious_vector); // software enable
}

// --- application-processor wakeup (INIT–SIPI–SIPI) --------------------------

/// Send an INIT IPI to the core with Local APIC id `apic_id` — the first step of
/// the wake sequence. Blocks until the LAPIC reports the IPI was delivered.
pub fn sendInit(apic_id: u32) void {
    write(register_icr_high, apic_id << 24);
    write(register_icr_low, 0x4500); // INIT, physical destination, assert, edge-triggered
    waitIcrIdle();
}

/// Send a STARTUP IPI (SIPI) telling the target core to begin executing at physical
/// address `vector << 12` (in real mode). Per the Intel bring-up protocol this is
/// sent twice after the INIT; both calls block until delivery completes.
pub fn sendStartup(apic_id: u32, vector: u8) void {
    write(register_icr_high, apic_id << 24);
    write(register_icr_low, 0x4600 | @as(u32, vector)); // STARTUP with the page vector
    waitIcrIdle();
}

fn waitIcrIdle() void {
    while (read(register_icr_low) & icr_delivery_pending != 0) {}
}

/// Send this core a fixed interrupt at `vector` (the "self" destination shorthand). A
/// device raises an MSI by writing its (address, data) to the LAPIC; with no such
/// device on QEMU's HPET, a self-IPI is the stand-in that lets the MSI vector-routing
/// path be tested end to end. Shorthand self (bits 19:18 = 01) | assert (bit 14).
pub fn selfIpi(vector: u8) void {
    write(register_icr_low, 0x4_4000 | @as(u32, vector));
    waitIcrIdle();
}

/// The calibration window: we time everything against a 10 ms reference interval.
const calib_ms = 10;

/// Measure the LAPIC timer's and the TSC's rates. The PIT (legacy 8254) can be
/// absent on UEFI Class 3 firmware — and polling it would hang — so we pick a
/// reference clock in order of preference: the CPU's own TSC frequency (CPUID leaf
/// 0x15, no external timer needed), then the discovered HPET, then the ACPI PM
/// timer, and only the PIT as a last resort. Each path yields the same two rates.
pub fn calibrate() void {
    var done = false;

    // 1. CPUID leaf 0x15 gives the TSC frequency directly — measure the LAPIC
    //    against the TSC itself, needing no external timer at all.
    if (cpuidTscHz()) |hz| {
        measure(hz, ~@as(u64, 0), rdtsc);
        tsc_hz = hz; // keep the exact enumerated value
        cal_source = "cpuid";
        done = true;
    }

    // 2. The discovered HPET.
    if (!done and configuration_hpet_base != 0) {
        if (hpetHz()) |hpet_hz| {
            measure(hpet_hz, hpetMask(), readHpet);
            cal_source = "hpet";
            done = true;
        }
    }

    // 3. The ACPI PM timer (fixed 3.579545 MHz).
    if (!done) {
        if (configuration_pm_timer) |pt| {
            measure(3_579_545, if (pt.is_32bit) 0xFFFF_FFFF else 0xFF_FFFF, readPmTimer);
            cal_source = "pm-timer";
            done = true;
        }
    }

    // 4. The legacy PIT, last resort.
    if (!done) {
        calibratePit();
        cal_source = "pit";
    }

    // A bad measurement (no reference actually ticked) leaves nonsense; fall back.
    if (ticks_per_ms == 0 or tsc_hz == 0) {
        calibratePit();
        cal_source = "pit";
    }

    tsc_base = rdtsc(); // the clock's zero point (boot)

    // Decide whether the TSC is trustworthy as a clocksource. Frequency (measured
    // above, possibly against the HPET/PIT) is necessary but not sufficient: the TSC
    // must also be *invariant* (CPUID 0x80000007 EDX[8]). AMD and modern Intel set
    // this; the bare qemu64 model does not.
    tsc_invariant = tscIsInvariant();

    // Bring up the HPET as a standby clocksource whenever one exists — even on the
    // CPUID-0x15 path where calibration never touched it — so a non-invariant TSC
    // (here) or an unsynchronized one (checkWarpSource, during SMP bring-up) can fall
    // back to a source that is immune to both. hpetHz() maps + enables the counter
    // and is idempotent if calibration already used it.
    if (configuration_hpet_base != 0) {
        if (hpetHz()) |hz| {
            hpet_clock_mask = hpetMask();
            if (hpet_clock_mask == ~@as(u64, 0)) { // only a 64-bit HPET is monotonic enough
                hpet_clock_hz = hz;
                hpet_clock_base = readHpet();
            }
        }
    }

    // Select the source: the fast TSC when invariant, else the HPET if we have one.
    // (checkWarpSource may still demote TSC -> HPET later if the cores' TSCs skew.)
    if (!tsc_invariant and hpet_clock_hz != 0) clock_source = .hpet;
}

/// Run the LAPIC timer one-shot from its maximum count while a monotonic reference
/// clock (frequency `ref_hz`, counter width `ref_mask`) counts out `calib_ms`, and
/// snapshot the TSC across the same window. Yields `ticks_per_ms` and `tsc_hz`.
fn measure(ref_hz: u64, ref_mask: u64, refNow: *const fn () u64) void {
    const calib_ticks = ref_hz / (1000 / calib_ms); // reference ticks in calib_ms

    write(register_timer_divide, timer_divide_16);
    write(register_lvt_timer, lvt_masked);
    write(register_timer_initial, 0xFFFFFFFF);

    const ref0 = refNow();
    const tsc0 = rdtsc();
    while (((refNow() -% ref0) & ref_mask) < calib_ticks) {}
    const tsc1 = rdtsc();

    const elapsed = 0xFFFFFFFF - read(register_timer_current);
    write(register_timer_initial, 0);

    ticks_per_ms = elapsed / calib_ms;
    tsc_hz = (tsc1 -% tsc0) * (1000 / calib_ms);
}

/// The PIT fallback (legacy 8254 channel 2, polled). Only reached when no better
/// reference exists — on a legacy-free machine this path isn't taken.
fn calibratePit() void {
    const pit_hz = 1_193_182;
    const pit_count: u16 = @intCast(pit_hz / 1000 * calib_ms);

    write(register_timer_divide, timer_divide_16);
    write(register_lvt_timer, lvt_masked);
    write(register_timer_initial, 0xFFFFFFFF);

    io.outb(0x61, io.inb(0x61) & 0xFC); // speaker off, gate low
    io.outb(0x43, 0xB0); // channel 2, lo/hi byte, mode 0
    io.outb(0x42, @truncate(pit_count));
    io.outb(0x42, @truncate(pit_count >> 8));

    const tsc_start = rdtsc();
    io.outb(0x61, (io.inb(0x61) & 0xFC) | 0x01); // gate high -> start
    var guard: u64 = 0;
    while (io.inb(0x61) & 0x20 == 0 and guard < 100_000_000) : (guard += 1) {} // bounded
    const tsc_end = rdtsc();

    const elapsed = 0xFFFFFFFF - read(register_timer_current);
    write(register_timer_initial, 0);

    ticks_per_ms = elapsed / calib_ms;
    tsc_hz = (tsc_end -% tsc_start) * (1000 / calib_ms);
}

// --- reference clocks ------------------------------------------------------

/// TSC frequency from CPUID leaf 0x15 (crystal_hz * numerator / denominator), or
/// null if the CPU doesn't enumerate it (common under QEMU, and on AMD).
fn cpuidTscHz() ?u64 {
    if (cpuid(0).eax < 0x15) return null;
    const r = cpuid(0x15);
    if (r.eax == 0 or r.ebx == 0 or r.ecx == 0) return null; // ratio/crystal not given
    return @as(u64, r.ecx) * r.ebx / r.eax;
}

/// Whether the CPU advertises an **invariant** TSC (CPUID leaf 0x80000007, EDX
/// bit 8) — the architectural guarantee, on both Intel and AMD, that the TSC ticks
/// at a constant rate across P/C-states and never stops. Requires the extended-leaf
/// range to reach 0x80000007 first.
fn tscIsInvariant() bool {
    if (cpuid(0x80000000).eax < 0x80000007) return false;
    return (cpuid(0x80000007).edx & (1 << 8)) != 0;
}

const CpuidRegs = struct { eax: u32, ebx: u32, ecx: u32, edx: u32 };

fn cpuid(leaf: u32) CpuidRegs {
    var a: u32 = undefined;
    var b: u32 = undefined;
    var c: u32 = undefined;
    var d: u32 = undefined;
    asm volatile ("cpuid"
        : [a] "={eax}" (a),
          [b] "={ebx}" (b),
          [c] "={ecx}" (c),
          [d] "={edx}" (d),
        : [leaf] "{eax}" (leaf),
          [sub] "{ecx}" (@as(u32, 0)),
    );
    return .{ .eax = a, .ebx = b, .ecx = c, .edx = d };
}

// HPET registers: capabilities at +0x00 (period in the high dword, in fs; bit 13 =
// 64-bit-counter capable), general configuration at +0x10, main counter at +0xF0.
fn hpetRead64(off: usize) u64 {
    return @as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).*;
}
fn hpetWrite64(off: usize, value: u64) void {
    @as(*volatile u64, @ptrFromInt(configuration_hpet_base + off)).* = value;
}

/// Whether the HPET has been mapped into the physmap yet, so `configuration_hpet_base`
/// already holds the virtual address. `hpetHz` is called more than once (calibration
/// may use the HPET, and the standby-clocksource setup asks for it again), and mapping
/// an already-mapped base a second time would double-offset it into an overflow.
var hpet_mapped: bool = false;

/// Map + enable the HPET and return its tick frequency, or null if unusable.
/// Maps the HPET into the physmap and switches configuration_hpet_base to that virtual
/// address, so the register accessors reach it without the identity map. Idempotent.
fn hpetHz() ?u64 {
    if (!hpet_mapped) {
        configuration_hpet_base = paging.mapMmio(configuration_hpet_base, 0x400, true);
        hpet_mapped = true;
    }
    const caps = hpetRead64(0x00);
    const period_fs = caps >> 32; // femtoseconds per tick
    if (period_fs == 0) return null;
    hpetWrite64(0x10, hpetRead64(0x10) | 1); // ENABLE_CNF: start the main counter
    return 1_000_000_000_000_000 / period_fs; // 1e15 fs/s ÷ fs/tick
}

/// The HPET counter width mask (64- or 32-bit, per caps bit 13).
fn hpetMask() u64 {
    return if (hpetRead64(0x00) & (1 << 13) != 0) ~@as(u64, 0) else 0xFFFF_FFFF;
}

fn readHpet() u64 {
    return hpetRead64(0xF0);
}

fn readPmTimer() u64 {
    const pt = configuration_pm_timer.?;
    // MMIO PM timer via the physmap (mapMmio is idempotent); the common case is
    // a legacy I/O port.
    if (pt.mmio) return @as(*volatile u32, @ptrFromInt(paging.mapMmio(pt.address, 4, false))).*;
    return io.inl(@intCast(pt.address));
}

/// Arm the LAPIC timer to fire on `timer_vector` at `hz` (periodic). Requires
/// calibrate() to have run.
pub fn initTimer(hz: u32) void {
    timer_hz = hz;
    const count = @as(u64, ticks_per_ms) * 1000 / hz; // counts per (1/hz) second
    write(register_timer_divide, timer_divide_16);
    write(register_lvt_timer, timer_vector | lvt_periodic);
    write(register_timer_initial, @intCast(count));
}

/// Configured periodic-interrupt frequency (Hz).
pub fn frequencyHz() u32 {
    return timer_hz;
}

/// Measured LAPIC timer frequency (Hz), for reporting/sanity checks.
pub fn lapicHz() u64 {
    return @as(u64, ticks_per_ms) * 1000;
}

/// Measured TSC frequency (Hz).
pub fn tscHz() u64 {
    return tsc_hz;
}

// Monotonic high-resolution clock. A function per resolution, each scaling the
// counter delta directly at its unit (the 128-bit intermediate avoids overflow
// across a long uptime). nanos() resolves to a few ns on the TSC; millis() is what
// the scheduler uses for sleep deadlines. The source is the TSC when it is invariant
// and synchronized, else the HPET counter (see clock_source) — the branch is one
// global load and the TSC path is unchanged from before.

/// The selected source's counter delta since its zero point.
fn clockCount() u64 {
    return switch (clock_source) {
        .tsc => rdtsc() -% tsc_base,
        // A 64-bit HPET (the only kind we select) never wraps in any realistic
        // uptime, so the wrapping subtraction is exact.
        .hpet => readHpet() -% hpet_clock_base,
    };
}

/// The selected source's frequency (0 if the clock is unavailable/uncalibrated).
fn clockHertz() u64 {
    return switch (clock_source) {
        .tsc => tsc_hz,
        .hpet => hpet_clock_hz,
    };
}

pub fn nanos() u64 {
    const hz = clockHertz();
    if (hz == 0) return 0;
    return @intCast(@as(u128, clockCount()) * 1_000_000_000 / hz);
}

pub fn micros() u64 {
    const hz = clockHertz();
    if (hz == 0) return 0;
    return @intCast(@as(u128, clockCount()) * 1_000_000 / hz);
}

pub fn millis() u64 {
    const hz = clockHertz();
    if (hz == 0) return 0;
    return @intCast(@as(u128, clockCount()) * 1_000 / hz);
}

/// Whether the CPU advertises an invariant TSC (CPUID 0x80000007 EDX[8]).
pub fn tscInvariant() bool {
    return tsc_invariant;
}

/// Test hook: force the TSC clocksource on, as if the CPU had advertised an invariant
/// TSC. QEMU's TCG accelerator (the only one for an x86 guest on an Apple-Silicon
/// host) does not expose the invariant-TSC bit — its emulated TSC isn't invariant — so
/// the tsc-sync test can't reach the real-Intel/AMD/KVM path through CPUID. This lets
/// that test exercise the TSC clocksource and the cross-core warp check anyway. tsc_base
/// is left as-is so the switch from the HPET is continuous.
pub fn forceTscClocksourceForTest() void {
    tsc_invariant = true;
    clock_source = .tsc;
}

/// How many per-AP warp checks actually ran (a rendezvous completed) — lets a test
/// confirm the cross-core check executed rather than being skipped.
pub fn warpChecksRun() u32 {
    return warp_checks;
}

/// Whether the per-core TSCs are synchronized (no backward warp seen at bring-up).
pub fn tscSynced() bool {
    return tsc_synced;
}

/// The active monotonic clocksource, for the boot log and tests.
pub fn clockSourceName() []const u8 {
    return switch (clock_source) {
        .tsc => "tsc",
        .hpet => "hpet",
    };
}

// --- cross-core TSC synchronization ("warp") check -------------------------
// Two cores hammer a shared "max seen" TSC value under a lock; if either reads a
// value below that max, its TSC lags the other's, and time would run backward for a
// task migrating between them (Linux calls this a warp). danos brings APs up one at a
// time, so this runs pairwise: the BSP (source) against each AP (target) as it comes
// online. It only matters — and only runs — while the TSC is the clocksource; on a
// machine already on the HPET (a bare VM) the whole rendezvous is skipped.

var warp_lock: u32 = 0;
var warp_last: u64 = 0;
var warp_bsp_ready: u32 = 0;
var warp_ap_ready: u32 = 0;
var warp_stop: u32 = 0;
var warp_checks: u32 = 0; // completed per-AP rendezvous count (for the tsc-sync test)

// The warp check is bounded by TIME, not iterations: a warp tick is a locked
// read-modify-write on a cacheline two cores are fighting over — microseconds
// under real contention, not the nanosecond an uncontended count assumes (a
// 1<<20-round budget measured 2-21 SECONDS per core on a 16-core machine), and
// a PAUSE costs ~140 cycles on modern Intel, so an iteration-counted await
// mis-measures by two orders of magnitude too. ~5 ms of pairwise hammering per
// core is plenty to catch a lagging TSC (Linux's check_tsc_warp budget), and
// ~100 ms is a generous rendezvous window for a healthy core.
const warp_check_ns: u64 = 5_000_000; // per-AP pairwise check duration
const warp_await_ns: u64 = 100_000_000; // rendezvous wait before giving up

/// TSC ticks for `ns` nanoseconds (valid whenever the warp check runs: the TSC
/// is the clocksource, so tsc_hz is calibrated).
fn warpTicksFor(ns: u64) u64 {
    return @intCast(@as(u128, ns) * tsc_hz / 1_000_000_000);
}

fn warpTick() void {
    while (@cmpxchgWeak(u32, &warp_lock, 0, 1, .acquire, .monotonic) != null) asm volatile ("pause");
    const t = rdtsc();
    if (t < warp_last) {
        const delta = warp_last - t;
        if (delta > tsc_warp_cycles) tsc_warp_cycles = delta;
        tsc_synced = false;
    } else {
        warp_last = t;
    }
    @atomicStore(u32, &warp_lock, 0, .release);
}

/// Spin (time-bounded) until `flag` is nonzero; false on timeout.
fn warpAwait(flag: *u32) bool {
    const deadline = rdtsc() +% warpTicksFor(warp_await_ns);
    while (@atomicLoad(u32, flag, .acquire) == 0) {
        if (rdtsc() -% deadline < (1 << 62)) return false; // past the deadline
        asm volatile ("pause");
    }
    return true;
}

/// BSP side of the pairwise TSC warp check, run once per AP as it reports in. No-op
/// unless the TSC is the active clocksource. If the AP's TSC proves to lag, demote
/// the monotonic clock to the HPET without a discontinuity.
pub fn checkWarpSource() void {
    if (clock_source != .tsc) return;
    warp_last = 0;
    @atomicStore(u32, &warp_stop, 0, .release);
    @atomicStore(u32, &warp_ap_ready, 0, .release);
    @atomicStore(u32, &warp_bsp_ready, 1, .release);
    if (!warpAwait(&warp_ap_ready)) { // AP never joined the rendezvous; skip, don't hang
        @atomicStore(u32, &warp_bsp_ready, 0, .release);
        return;
    }
    const deadline = rdtsc() +% warpTicksFor(warp_check_ns);
    while (rdtsc() -% deadline >= (1 << 62)) warpTick(); // until the time budget is spent
    @atomicStore(u32, &warp_stop, 1, .release);
    @atomicStore(u32, &warp_bsp_ready, 0, .release);
    warp_checks += 1;

    if (!tsc_synced and hpet_clock_hz != 0) demoteToHpet();
}

/// AP side: join the BSP's warp check, then return so the core can enter the
/// scheduler. Bounded so a missing BSP can't strand the core.
pub fn checkWarpTarget() void {
    if (clock_source != .tsc) return;
    if (!warpAwait(&warp_bsp_ready)) return;
    @atomicStore(u32, &warp_ap_ready, 1, .release);
    // The BSP owns the budget; this bound only protects against a lost BSP.
    const deadline = rdtsc() +% warpTicksFor(2 * warp_check_ns + warp_await_ns);
    while (@atomicLoad(u32, &warp_stop, .acquire) == 0) {
        if (rdtsc() -% deadline < (1 << 62)) return; // past the deadline
        warpTick();
    }
}

/// Switch the clocksource from the TSC to the HPET without a discontinuity: choose
/// the HPET zero point so it reads the same nanosecond value the TSC does right now,
/// so time neither jumps nor runs backward across the switch. Called when the warp
/// check proves the per-core TSCs unsynchronized.
fn demoteToHpet() void {
    const now_ns = @as(u128, rdtsc() -% tsc_base) * 1_000_000_000 / tsc_hz;
    const equivalent_ticks: u64 = @intCast(now_ns * hpet_clock_hz / 1_000_000_000);
    hpet_clock_base = readHpet() -% equivalent_ticks;
    clock_source = .hpet;
}

/// Acknowledge the current interrupt so the LAPIC will deliver the next one.
pub fn eoi() void {
    write(register_eoi, 0);
}

/// Optional callback run each tick (the scheduler registers it for preemption).
var on_tick: ?*const fn () void = null;

pub fn setTickHook(hook: *const fn () void) void {
    on_tick = hook;
}

/// The timer interrupt handler: advance the monotonic tick count, then run the
/// tick hook (which may switch tasks). The interrupt is already acknowledged by
/// the dispatcher before we get here, so a task switch here doesn't stall it.
pub fn timerTick() void {
    // Acknowledge before the tick hook: `on_tick` is the scheduler, which may switch
    // tasks and not return promptly, and the LAPIC mustn't wait on it to deliver the
    // next interrupt. (Each device handler now owns its own EOI — see
    // `idt.interruptDispatch` — because a *routed* interrupt must be masked at the
    // I/O APIC before it is acknowledged, an ordering the dispatcher can't impose.)
    eoi();
    tick_count +%= 1;
    if (on_tick) |hook| hook();
}

/// This core's Local APIC id — the interrupt destination for `routeGsi`.
pub fn localId() u8 {
    return @truncate(read(register_id) >> 24);
}

/// Number of timer ticks so far. Volatile load: the count is bumped
/// asynchronously by the interrupt handler, so callers must re-read memory.
pub fn ticks() u64 {
    return @as(*const volatile u64, &tick_count).*;
}
