starkernel: converge the tick path and wire the adaptive heartbeat (item 0.8)

Introduces src/starkernel/heartbeat.c as the shared top/bottom-half
implementation of heartbeat_init/tick/service/ticks/trust/state, replacing
the per-architecture duplicates in amd64/riscv64/aarch64 timer.c. Each
arch's timer.c now contributes only heartbeat_read_counter() (rdtsc /
rdtime / CNTPCT_EL0). Per the GAP-A1 ruling the top half stays counter+
latch only; heartbeat_service() (called every REPL idle iteration,
unconditionally per FABRIC.md's fidelity note) does the window/variance/
trust work outside interrupt context. vm_tick()'s call sites are
unchanged -- the engine still runs on the virtual tick.

Per FABRIC.md §26 (ruled 2026-08-03): wires Loop #7's execution-derived
stable/volatile signal into the physical re-arm period. vm_runtime.c's
existing Loop #7 site now calls heartbeat_set_adaptive_period_ns() with
tick_target_ns ratio-rescaled onto a 10ms kernel base (not the hosted
10us HEARTBEAT_TICK_NS -- see §26.3 for the scale mismatch). Each
architecture's re-arm function (apic_timer_rearm() on amd64/aarch64,
riscv64_timer_rearm()) now converts heartbeat_next_period_ns() to its
own raw counter units instead of a fixed constant; amd64 gained a
rearm function it didn't previously need, since periodic-mode auto-reload
never required one before this item.

Verified: all three architectures build with no new warnings and boot
cleanly to ok> with dict_hash=0x3d4e1daf289da94f, unchanged from the
pre-change baseline -- no regression. Verified NOT achieved: live re-arm
period variation under load. A temporary diagnostic (added and reverted)
confirmed Loop #7 never actually fired during a live QEMU session -- a
synthetic word-execution loop drove ~6,500 executions, past the 1000-tick
inference frequency, without tripping vm_tick_inference_engine()'s
pre-existing !vm->rolling_window.is_warm gate. That gate predates this
item and was not investigated -- out of scope. FABRIC.md's Done-when is
amended to record this honestly rather than claim it.

Punch list §25 item 0.8 complete (per amended, weaker acceptance -- see
the item's own annotation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-04 00:01:48 -04:00
co-authored by Claude Sonnet 5
parent da4cb14702
commit 3699be964d
23 changed files with 42170 additions and 449 deletions
+16 -1
View File
@@ -32,6 +32,7 @@
#include "apic.h"
#include "uefi.h"
#include "console.h"
#include "timer.h"
#include <stdint.h>
/* Defined in arch.c (item 0.4). Same extern-in-place convention as
@@ -92,6 +93,7 @@ static inline uint32_t mmio_read32(uintptr_t base, uint32_t offset)
static uint32_t s_timer_ppi = TIMER_PPI_EL1;
static uint64_t s_timer_period_tsc = 0;
static uint64_t s_counter_hz_apic = 0; /* CNTFRQ_EL0 (item 0.8/§26) */
/**
* @brief Initialise the GICv2 distributor and CPU interface (M4 milestone).
@@ -227,6 +229,7 @@ int apic_timer_init(uint64_t tsc_hz, uint32_t tick_hz)
} else {
s_timer_period_tsc = 10000000;
}
s_counter_hz_apic = tsc_hz;
return 0;
}
@@ -305,11 +308,23 @@ void apic_timer_start(void)
* @c heartbeat_tick() — same ordering discipline as
* @c riscv64_timer_rearm() (item 0.3): re-arm first, so a fault in the
* rest of the handler cannot also cost the next tick.
*
* The step written is @c heartbeat_next_period_ns() (item 0.8, §26)
* converted to @c CNTPCT_EL0 ticks via @c s_counter_hz_apic, not the fixed
* @c s_timer_period_tsc @c apic_timer_start() used for the very first arm
* -- that value remains @c apic_timer_period_tsc()'s return for
* @c heartbeat_init()'s initial @c expected_delta, unchanged.
*/
void apic_timer_rearm(void)
{
uint64_t period_ticks = (s_counter_hz_apic > 0)
? (heartbeat_next_period_ns() * s_counter_hz_apic) / 1000000000ULL
: s_timer_period_tsc;
if (period_ticks == 0) {
period_ticks = 1;
}
/* uint64_t, zero-extended -- see apic_timer_start() for why. */
uint64_t tval = (uint64_t)(uint32_t)s_timer_period_tsc;
uint64_t tval = (uint64_t)(uint32_t)period_ticks;
if (aarch64_current_el() == 2) {
__asm__ volatile ("msr cnthp_tval_el2, %0" :: "r"(tval));