Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization: - Move StarForth v3 engine to v3/ (src/, include/, Makefile) - Move kernel to kernel/ (src/, include/, linker/, Makefile) - Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md) - Move FABRIC-0..4.md to docs/fabric/ - Move ONTOLOGY.md and ROADMAP.md to docs/ Board infrastructure: - Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/ - Each board has board.mk (ISA, CPU flags, boot recipe) and README.md - Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET - make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board - ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet) - scripts/mkdiskimage.sh builds disk images for all boards Docs pipeline: - docs/book/ with LaTeX master (main.tex) and Makefile - pandoc converts Markdown to LaTeX at build time - Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks) - make docs builds single PDF (754 pages, 0 missing characters) - make docs TARGET=<board> adds board appendix - build/docs/<book|board>/meta.tex stamps git commit into PDF Bug fixes: - 42 include paths that only worked by accident now use correct relative paths - clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build) - Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved - Doxyfile, .clang-tidy, README.md, Kconfig paths updated Verified: - Hosted v3 build passes 1012 tests, 0 failures - SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE) - Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes - make clean TARGET=<board> removes only that board and its ISA objects - make all builds all boards, hosted v3, and docs in one run Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
StarForth — Steady-State Virtual Machine Runtime
|
||||
|
||||
Copyright (c) 2023–2025 Robert A. James
|
||||
All rights reserved.
|
||||
|
||||
This file is part of the StarForth project.
|
||||
|
||||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* capsule_vm_physics.c - Dynamic VM Fleet Physics Implementation
|
||||
*
|
||||
* See capsule_vm_physics.h and
|
||||
* docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
|
||||
* for the full design rationale.
|
||||
*
|
||||
* Concurrency: no locking. The only interrupt-driven kernel path
|
||||
* (heartbeat_tick, from the timer ISR) touches only its own isolated
|
||||
* TimeTrustState and never calls into mama_word_*, vm_interpret, or any
|
||||
* capsule/dictionary state. HEARTBEAT_THREAD_ENABLED=0 means no separate
|
||||
* OS thread runs physics logic either. BIRTH/KILL/VM-EXEC/VM-CALL/
|
||||
* VM-STEP all execute synchronously in Hera's single interpreter
|
||||
* context with nothing able to preempt them mid-transfer.
|
||||
*
|
||||
* L8 coupling (rev q): the only place this file reaches outside its own
|
||||
* VMPhysics/VMFleetWindow state is l8_regime_modulation_q16(), a
|
||||
* read-only look at Hera's own ssm_l8_state via sk_get_mama_vm() --
|
||||
* modulating the fleet's empirically-recovered transfer rate uniformly,
|
||||
* never deciding which VM receives heat. Still a passive observer: this
|
||||
* changes how fast the fleet's own rate estimate responds, not who runs
|
||||
* or who gets touched.
|
||||
*/
|
||||
|
||||
#include "starkernel/capsule_vm_physics.h"
|
||||
#include "starkernel/capsule_birth.h"
|
||||
#include "starkernel/kmalloc.h"
|
||||
#include "starkernel/q48_16.h"
|
||||
#include "starkernel/console.h"
|
||||
#include "starforth_config.h" /* STADIUM_CAPACITY_TICK */
|
||||
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
|
||||
#include "vm.h" /* VM, vm->ssm_l8_state */
|
||||
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
|
||||
|
||||
/* Word-level ROLLING_WINDOW_SIZE defaults to 4096 because word executions
|
||||
* number in the millions. Fleet touches are real VM-EXEC/VM-CALL/VM-STEP
|
||||
* dispatches -- dozens to low hundreds over a full boot + acceptance run.
|
||||
* 64 gives the regression enough samples to stabilize without being
|
||||
* wastefully oversized for a 3-8 VM fleet. Compile-time constant, same
|
||||
* tuning-knob convention as ROLLING_WINDOW_SIZE. */
|
||||
#define VM_FLEET_WINDOW_DEPTH 64
|
||||
|
||||
/* |fleet_heat_sum - Q48_ONE| < 5% of Q48_ONE, mirrors fleet-k.4th's
|
||||
* K-EPSILON (3277). */
|
||||
#define VM_PHYSICS_EPSILON_Q48 3277ULL
|
||||
|
||||
typedef struct {
|
||||
uint64_t execution_heat_q48;
|
||||
uint64_t last_active_tick; /* fleet_heartbeat_tick_count value at last touch */
|
||||
int touched; /* 0 until the first touch; explicit rather than
|
||||
* overloading last_active_tick == 0, which is a
|
||||
* legitimate tick value a first touch can land on */
|
||||
int is_live;
|
||||
} VMPhysics;
|
||||
|
||||
typedef struct vm_physics_node {
|
||||
VMUuid vm_id;
|
||||
VMPhysics physics;
|
||||
struct vm_physics_node *next;
|
||||
} vm_physics_node_t;
|
||||
|
||||
/* kmalloc-backed linked list, same pattern as capsule_birth.c's
|
||||
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
|
||||
static vm_physics_node_t *vm_physics_head = (void *)0;
|
||||
|
||||
/* One recorded touch's inputs to the transfer law (amount = elapsed_ticks *
|
||||
* slope >> 16, clamped to available heat) -- not a derived heat value.
|
||||
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
|
||||
* heat-snapshot design (rev m) with this: since the transfer law is known
|
||||
* exactly, the true rate can be recovered directly from any touch that
|
||||
* wasn't clamped (rate = amount / elapsed_ticks), rather than curve-fitting
|
||||
* a reconstructed trajectory against a synthetic time axis.
|
||||
*
|
||||
* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this used
|
||||
* to be elapsed_us, wall-clock microseconds -- non-reproducible under TCG
|
||||
* per GAP-A1 (FABRIC-0.md §25.7.1). Now counts fleet_heartbeat_tick_count
|
||||
* ticks, which is execution-paced (incremented once per vm_tick() call,
|
||||
* confirmed at vm_runtime.c:143). */
|
||||
typedef struct {
|
||||
uint64_t elapsed_ticks; /* since this vm_id's previous touch; 0 if there
|
||||
* was no previous touch to compare against */
|
||||
uint64_t amount; /* elapsed_ticks * slope >> 16 at touch time,
|
||||
* i.e. the rate-implied transfer before any
|
||||
* clamp; 0 if not computed (first touch, zero
|
||||
* slope, or non-monotonic tick) */
|
||||
int clamped; /* 1 if the heat actually moved was capped below
|
||||
* `amount` by what the rest of the fleet held --
|
||||
* carries no rate information, excluded from
|
||||
* the estimator rather than treated as a zero */
|
||||
} VMFleetTouchSample;
|
||||
|
||||
typedef struct {
|
||||
VMFleetTouchSample touch_samples[VM_FLEET_WINDOW_DEPTH];
|
||||
uint32_t head;
|
||||
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
|
||||
int is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
|
||||
} VMFleetWindow;
|
||||
|
||||
/* One instance, fleet-wide -- not per-VM state. */
|
||||
static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
|
||||
|
||||
/* Below this many informative (unclamped) samples in a full window, skip
|
||||
* the fit and keep the current slope rather than trust a handful of
|
||||
* points -- same "skip, don't substitute a degenerate value" philosophy
|
||||
* as the is_warm gate. Arbitrary but conservative relative to the
|
||||
* 64-deep window: needs an eighth of the window to be informative. */
|
||||
#define VM_PHYSICS_MIN_INFORMATIVE_SAMPLES 8
|
||||
|
||||
/* Shared adaptive slope, fit from aggregate fleet statistics (one slope
|
||||
* for the whole fleet), exactly mirroring how the word engine infers one
|
||||
* decay_slope_q48 per VM instance rather than one per word.
|
||||
*
|
||||
* Seeded at 65536/3 (Q48.16 for 1/3), matching the word engine's actual
|
||||
* bootstrap value (src/vm_bootstrap.c / src/starkernel/vm/vm_bootstrap.c:
|
||||
* "vm->decay_slope_q48 = (1ULL << 16) / 3"). The old comment on that field
|
||||
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
|
||||
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
|
||||
* here. This was previously seeded at literal 0, which cannot bootstrap:
|
||||
* vm_physics_touch's transfer amount is elapsed_ticks * slope >> 16, so a
|
||||
* zero slope moves zero heat on every touch forever, which starves
|
||||
* vm_physics_tick's regression of the very heat-trajectory signal it
|
||||
* needs to infer a better slope -- a closed loop with no way out. Found
|
||||
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
|
||||
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f).
|
||||
*
|
||||
* UNVALIDATED after the ns-to-tick restatement (FABRIC-0.md item 2.1,
|
||||
* 2026-08-04): this value was calibrated against elapsed wall-clock
|
||||
* microseconds. Elapsed fleet-heartbeat *ticks* between touches is a
|
||||
* different quantity at a different scale, and the seed has not been
|
||||
* re-fit against it. Left as-is rather than guessed at -- the acceptance
|
||||
* boot test below only checks determinism, not transfer magnitude; a real
|
||||
* re-tune is DoE work (item 5.1), not something to invent here. */
|
||||
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
|
||||
static uint64_t slope_fit_quality_q48 = 0;
|
||||
|
||||
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
|
||||
* HeartbeatState.tick_count -- see the rationale in the header. Advances
|
||||
* from whichever VM's own heartbeat cycle happens to fire, so the
|
||||
* readiness signal reflects the fleet's aggregate activity rather than
|
||||
* any one VM's (in practice, Hera's) personal word-execution rate.
|
||||
* Declared here (rather than just above vm_physics_heartbeat_tick, where
|
||||
* it used to live) so vm_physics_touch() can read the current tick
|
||||
* without a forward declaration -- it is now this counter's second
|
||||
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
|
||||
static uint64_t fleet_heartbeat_tick_count = 0;
|
||||
static uint64_t fleet_last_capacity_tick = 0; /* renamed 2026-08-15 from
|
||||
fleet_last_inference_tick -- gated on
|
||||
STADIUM_CAPACITY_TICK now, not HEARTBEAT_
|
||||
INFERENCE_FREQUENCY, see vm_physics_heartbeat_tick() */
|
||||
|
||||
static vm_physics_node_t *vm_physics_find(VMUuid vm_id)
|
||||
{
|
||||
vm_physics_node_t *n = vm_physics_head;
|
||||
while (n) {
|
||||
if (vm_uuid_equal(n->vm_id, vm_id)) return n;
|
||||
n = n->next;
|
||||
}
|
||||
return (void *)0;
|
||||
}
|
||||
|
||||
/**
|
||||
* vm_physics_transfer - The one conservative primitive everything else
|
||||
* is a special case of. Subtracts amount from from (clamped at 0) and
|
||||
* adds the same amount to to. Nothing is created or destroyed:
|
||||
* sum(execution_heat for all LIVE VMs) is invariant across any call.
|
||||
*/
|
||||
static void vm_physics_transfer(VMPhysics *from, VMPhysics *to, uint64_t amount_q48)
|
||||
{
|
||||
uint64_t moved = (amount_q48 > from->execution_heat_q48)
|
||||
? from->execution_heat_q48
|
||||
: amount_q48;
|
||||
from->execution_heat_q48 -= moved;
|
||||
to->execution_heat_q48 += moved;
|
||||
}
|
||||
|
||||
void vm_physics_init(VMUuid vm_id)
|
||||
{
|
||||
vm_physics_node_t *node = vm_physics_find(vm_id);
|
||||
/* Hera (vm_uuid_hera()) is the fleet's single structural root -- the one
|
||||
* entry capsule_vm_kill refuses to ever kill, and the same fixed
|
||||
* point vm_physics_find_root_id below walks every parent chain up
|
||||
* to. She is where the fleet's whole Q48_ONE initially resides;
|
||||
* every other VM joins an already-nonzero fleet and starts at 0,
|
||||
* per the "cold mass added to a closed system" reasoning -- which
|
||||
* requires a nonzero sum to already exist, so Hera's own
|
||||
* registration is where that sum is seeded. */
|
||||
int is_root = vm_uuid_is_hera(vm_id);
|
||||
|
||||
if (node) {
|
||||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||||
node->physics.last_active_tick = 0;
|
||||
node->physics.touched = 0;
|
||||
node->physics.is_live = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
node = (vm_physics_node_t *)kmalloc(sizeof(vm_physics_node_t));
|
||||
if (!node) return;
|
||||
|
||||
node->vm_id = vm_id;
|
||||
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
|
||||
node->physics.last_active_tick = 0;
|
||||
node->physics.touched = 0;
|
||||
node->physics.is_live = 1;
|
||||
node->next = vm_physics_head;
|
||||
vm_physics_head = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* vm_physics_find_root_id - Walk parent_vm_id edges up to Hera
|
||||
*
|
||||
* Every VM's parent_vm_id (VMRegistryEntry, capsule_run.h) is set once
|
||||
* at its own birth and never rewritten -- including a dead VM's, so the
|
||||
* walk continues correctly through an already-killed intermediate
|
||||
* parent without any special dead-parent handling. Hera's own entry is
|
||||
* self-referential (parent_vm_id == vm_id == vm_uuid_hera()), which is
|
||||
* what stops the walk: she is the only node with no outbound edge.
|
||||
*
|
||||
* The 64-hop guard is defensive only -- a well-formed chain terminates
|
||||
* in O(fleet depth) hops, nowhere near 64. It exists so a corrupted
|
||||
* chain (which should never happen) returns the starting vm_id instead
|
||||
* of spinning.
|
||||
*/
|
||||
static VMUuid vm_physics_find_root_id(VMUuid vm_id)
|
||||
{
|
||||
VMUuid cursor = vm_id;
|
||||
uint32_t guard;
|
||||
|
||||
for (guard = 0; guard < 64; guard++) {
|
||||
VMRegistryEntry entry;
|
||||
|
||||
if (capsule_vm_registry_get(cursor, &entry) != 0) break;
|
||||
if (vm_uuid_equal(entry.parent_vm_id, cursor)) break; /* self-referential: root */
|
||||
cursor = entry.parent_vm_id;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
void vm_physics_retire(VMUuid vm_id)
|
||||
{
|
||||
vm_physics_node_t *dying = vm_physics_find(vm_id);
|
||||
vm_physics_node_t *root;
|
||||
VMUuid root_id;
|
||||
|
||||
if (!dying || !dying->physics.is_live) return;
|
||||
|
||||
root_id = vm_physics_find_root_id(vm_id);
|
||||
root = !vm_uuid_equal(root_id, vm_id) ? vm_physics_find(root_id) : (void *)0;
|
||||
|
||||
if (root) {
|
||||
/* The dying VM's entire remaining heat flows to the root it
|
||||
* chains up to (in practice always Hera today) -- no division,
|
||||
* no weighting policy, no "no survivors" case, since a root is
|
||||
* always there by construction. This is also what makes
|
||||
* TRIPOD-TEST's kill-then-rebirth "K soak" check meaningful: a
|
||||
* root VM (e.g. Hermes) being killed and later respawned by
|
||||
* Hera doesn't lose heat across the gap, it just parks at Hera
|
||||
* in the meantime. */
|
||||
vm_physics_transfer(&dying->physics, &root->physics,
|
||||
dying->physics.execution_heat_q48);
|
||||
}
|
||||
/* root_id == vm_id means the dying VM IS the root itself (Hera --
|
||||
* shouldn't happen, she can't be killed) or the chain was broken
|
||||
* (shouldn't happen, see vm_physics_find_root_id). Either way
|
||||
* there's nowhere conservation-preserving to send the remainder;
|
||||
* it is dropped along with the dying entry. */
|
||||
|
||||
dying->physics.execution_heat_q48 = 0;
|
||||
dying->physics.is_live = 0;
|
||||
}
|
||||
|
||||
void vm_physics_touch(VMUuid vm_id)
|
||||
{
|
||||
vm_physics_node_t *target = vm_physics_find(vm_id);
|
||||
VMFleetTouchSample sample = { 0, 0, 0 };
|
||||
uint64_t now_tick = fleet_heartbeat_tick_count;
|
||||
|
||||
if (!target || !target->physics.is_live) return;
|
||||
|
||||
/* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this
|
||||
* used to gate on target->physics.last_active_ns > 0 and scale by
|
||||
* elapsed wall-clock microseconds -- non-reproducible run to run under
|
||||
* TCG (GAP-A1, FABRIC-0.md §25.7.1). fleet_heartbeat_tick_count is
|
||||
* execution-paced (vm_runtime.c:143), so elapsed_ticks is now a pure
|
||||
* function of the execution stream. `touched` replaces the old
|
||||
* `> 0` sentinel: tick 0 is a legitimate value for a genuine first
|
||||
* touch to land on, so "never touched" needs its own flag rather than
|
||||
* overloading the tick value. */
|
||||
if (fleet_transfer_slope_q48 > 0 &&
|
||||
target->physics.touched &&
|
||||
now_tick > target->physics.last_active_tick) {
|
||||
|
||||
uint64_t elapsed_ticks = now_tick - target->physics.last_active_tick;
|
||||
uint64_t amount = (elapsed_ticks * fleet_transfer_slope_q48) >> 16;
|
||||
|
||||
sample.elapsed_ticks = elapsed_ticks;
|
||||
sample.amount = amount;
|
||||
|
||||
if (amount > 0) {
|
||||
/* Pull toward the touched VM, proportional to each other LIVE
|
||||
* VM's current heat -- same fan-out shape as retire, mirror
|
||||
* direction. Clamped to what the rest of the fleet actually
|
||||
* holds, so this can never manufacture heat. */
|
||||
vm_physics_node_t *n;
|
||||
uint64_t others_total = 0;
|
||||
uint64_t moved_total;
|
||||
|
||||
for (n = vm_physics_head; n; n = n->next) {
|
||||
if (n != target && n->physics.is_live) {
|
||||
others_total += n->physics.execution_heat_q48;
|
||||
}
|
||||
}
|
||||
|
||||
/* moved_total == 0 when others_total == 0 (nothing to pull --
|
||||
* the fleet is already fully concentrated elsewhere), handled
|
||||
* uniformly by the same clamp as a partial cap: either way
|
||||
* the actual transfer fell short of what the rate implied, so
|
||||
* this touch carries no usable rate information (rev o). */
|
||||
moved_total = (amount > others_total) ? others_total : amount;
|
||||
sample.clamped = (moved_total != amount);
|
||||
|
||||
if (moved_total > 0) {
|
||||
for (n = vm_physics_head; n; n = n->next) {
|
||||
if (n != target && n->physics.is_live) {
|
||||
uint64_t share = (moved_total * n->physics.execution_heat_q48)
|
||||
/ others_total;
|
||||
vm_physics_transfer(&n->physics, &target->physics, share);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target->physics.last_active_tick = now_tick;
|
||||
target->physics.touched = 1;
|
||||
|
||||
/* Record this touch's transfer-law inputs, not a derived heat value
|
||||
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
|
||||
* VMFleetTouchSample for why. */
|
||||
fleet_window.touch_samples[fleet_window.head] = sample;
|
||||
fleet_window.head = (fleet_window.head + 1) % VM_FLEET_WINDOW_DEPTH;
|
||||
if (fleet_window.count < VM_FLEET_WINDOW_DEPTH) {
|
||||
fleet_window.count++;
|
||||
}
|
||||
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
|
||||
*
|
||||
* Gives L8 a real, passive-observer-respecting causal channel into the
|
||||
* fleet's transfer rate (rev q, VM-FLEET-ATTRACTOR-DESIGN-20260705.md):
|
||||
* previously nothing L8 did could affect fleet dynamics at all (rev j's
|
||||
* architectural diagnosis for its null DoE result). Reuses Hera's
|
||||
* adaptive table's current_regime -- the same 3-bit entropy/cv/temporal
|
||||
* classification the bandit already stratifies its own scores by,
|
||||
* rather than inventing a new signal -- as a proxy for fleet-wide
|
||||
* dispatch activity, since all VM-EXEC/VM-CALL flows through her.
|
||||
*
|
||||
* modulation = (popcount(regime) + 1) * 0.5, i.e. 0.5x-2.0x: more of the
|
||||
* three high-signal bits active scales the fleet's already-recovered
|
||||
* rate up (track change more aggressively), fewer scales it down (damp
|
||||
* toward the raw empirical rate). No per-VM favoritism -- this scales
|
||||
* whatever vm_physics_tick() already computed uniformly, it doesn't
|
||||
* decide who receives heat.
|
||||
*
|
||||
* Returns Q16 65536 (neutral 1.0x) if Hera or her adaptive table isn't
|
||||
* available, so a missing signal never blocks or corrupts the estimate
|
||||
* this multiplies.
|
||||
*/
|
||||
static uint32_t l8_regime_modulation_q16(void)
|
||||
{
|
||||
VM *mama = (VM*)sk_get_mama_vm();
|
||||
ssm_l8_state_t *l8;
|
||||
uint32_t popcount;
|
||||
|
||||
if (!mama || !mama->ssm_l8_state) return 65536u;
|
||||
|
||||
l8 = (ssm_l8_state_t*)mama->ssm_l8_state;
|
||||
if (!l8->table) return 65536u; /* legacy path tracks no regime */
|
||||
|
||||
popcount = (uint32_t)(((l8->table->current_regime >> 2) & 1u) +
|
||||
((l8->table->current_regime >> 1) & 1u) +
|
||||
(l8->table->current_regime & 1u));
|
||||
return (popcount + 1u) * 32768u;
|
||||
}
|
||||
|
||||
void vm_physics_tick(uint64_t now_ns)
|
||||
{
|
||||
uint64_t rates[VM_FLEET_WINDOW_DEPTH];
|
||||
uint32_t rate_count = 0;
|
||||
uint32_t i;
|
||||
|
||||
(void)now_ns; /* Unlike the log-linear regression this replaced, the
|
||||
* estimator below needs no external time reference --
|
||||
* each sample already carries its own elapsed_ticks. */
|
||||
|
||||
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
|
||||
* substitute a default slope. Expected to stay unwarmed (or warm
|
||||
* with a low-quality fit) at the current 3-VM Tripod scale -- there
|
||||
* simply aren't enough distinct touch events yet. */
|
||||
if (!fleet_window.is_warm) return;
|
||||
|
||||
/* Direct rate recovery, not curve-fitting (rev o,
|
||||
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
|
||||
* OLS approach of rev b-n): the transfer law is known exactly
|
||||
* (amount = elapsed_ticks * slope >> 16), so any unclamped touch can
|
||||
* be inverted directly for an exact per-sample rate. Order doesn't
|
||||
* matter here -- a median over the observation window, not a
|
||||
* time-indexed fit -- so this walks the fixed-size array directly
|
||||
* rather than replaying ring order from fleet_window.head. */
|
||||
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
|
||||
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
|
||||
|
||||
if (s->clamped || s->amount == 0 || s->elapsed_ticks == 0) continue;
|
||||
|
||||
rates[rate_count++] = (s->amount << 16) / s->elapsed_ticks;
|
||||
}
|
||||
|
||||
/* slope_fit_quality_q48 as the informative fraction of the window --
|
||||
* a real measure now, not a fixed placeholder. Reported even when
|
||||
* there's too little to fit, same as a partial-sample regression
|
||||
* would report low confidence rather than nothing at all. */
|
||||
slope_fit_quality_q48 = ((uint64_t)rate_count << 16) / VM_FLEET_WINDOW_DEPTH;
|
||||
|
||||
/* Too few informative samples to trust: keep the current slope
|
||||
* rather than overwrite it with an estimate from a handful of
|
||||
* points (or zero) -- the same "skip, don't substitute a degenerate
|
||||
* value" philosophy as the is_warm gate above. */
|
||||
if (rate_count < VM_PHYSICS_MIN_INFORMATIVE_SAMPLES) return;
|
||||
|
||||
/* Insertion sort for the median -- rate_count <= VM_FLEET_WINDOW_DEPTH
|
||||
* (64), no library qsort in this freestanding kernel-only file. */
|
||||
for (i = 1; i < rate_count; i++) {
|
||||
uint64_t key = rates[i];
|
||||
uint32_t j = i;
|
||||
|
||||
while (j > 0 && rates[j - 1] > key) {
|
||||
rates[j] = rates[j - 1];
|
||||
j--;
|
||||
}
|
||||
rates[j] = key;
|
||||
}
|
||||
|
||||
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
|
||||
}
|
||||
|
||||
/* Gated on STADIUM_CAPACITY_TICK, not HEARTBEAT_INFERENCE_FREQUENCY (fixed
|
||||
* 2026-08-15, FABRIC-1.md §12 Q5): fleet_heartbeat_tick_count is fed by
|
||||
* EVERY live VM's own vm_tick() (see the comment above this counter's
|
||||
* declaration), so it advances several times faster in wall-clock terms
|
||||
* than a single VM's own tick_count -- comparing it against
|
||||
* HEARTBEAT_INFERENCE_FREQUENCY (written assuming a single VM's stream)
|
||||
* meant this fired far more often than §22.4's required ~1000:1 separation
|
||||
* from a VM's own heat-inference loop. Still one clock, one counter --
|
||||
* this is a bigger flat threshold on the same counter, not a second tick
|
||||
* source or anything that reads live VM count to adjust itself. */
|
||||
void vm_physics_heartbeat_tick(uint64_t now_ns)
|
||||
{
|
||||
fleet_heartbeat_tick_count++;
|
||||
if ((fleet_heartbeat_tick_count - fleet_last_capacity_tick) >= STADIUM_CAPACITY_TICK) {
|
||||
vm_physics_tick(now_ns);
|
||||
fleet_last_capacity_tick = fleet_heartbeat_tick_count;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t vm_physics_fleet_heat_sum(void)
|
||||
{
|
||||
uint64_t total = 0;
|
||||
vm_physics_node_t *n;
|
||||
|
||||
for (n = vm_physics_head; n; n = n->next) {
|
||||
if (n->physics.is_live) {
|
||||
total += n->physics.execution_heat_q48;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int vm_physics_conserved(void)
|
||||
{
|
||||
uint64_t sum = vm_physics_fleet_heat_sum();
|
||||
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
|
||||
return diff < VM_PHYSICS_EPSILON_Q48;
|
||||
}
|
||||
|
||||
uint64_t vm_physics_heat_of(VMUuid vm_id)
|
||||
{
|
||||
vm_physics_node_t *n = vm_physics_find(vm_id);
|
||||
if (!n || !n->physics.is_live) return 0;
|
||||
return n->physics.execution_heat_q48;
|
||||
}
|
||||
|
||||
/* Decimal print, same shape as parity.c's parity_put_u64 -- freestanding,
|
||||
* no snprintf dependency. */
|
||||
static void console_put_u64(uint64_t val)
|
||||
{
|
||||
char buf[21];
|
||||
int i = 20;
|
||||
|
||||
buf[i--] = '\0';
|
||||
if (val == 0) {
|
||||
buf[i--] = '0';
|
||||
} else {
|
||||
while (val > 0 && i >= 0) {
|
||||
buf[i--] = (char)('0' + (val % 10));
|
||||
val /= 10;
|
||||
}
|
||||
}
|
||||
console_puts(&buf[i + 1]);
|
||||
}
|
||||
|
||||
void vm_physics_status(void)
|
||||
{
|
||||
uint64_t sum = vm_physics_fleet_heat_sum();
|
||||
|
||||
console_puts("VM-PHYSICS: fleet_heat_sum=");
|
||||
console_put_u64(sum);
|
||||
console_println("");
|
||||
|
||||
console_puts("VM-PHYSICS: conserved=");
|
||||
console_println(vm_physics_conserved() ? "CONSERVED" : "DRIFTED");
|
||||
|
||||
console_puts("VM-PHYSICS: fleet_transfer_slope_q48=");
|
||||
console_put_u64(fleet_transfer_slope_q48);
|
||||
console_println("");
|
||||
|
||||
console_puts("VM-PHYSICS: slope_fit_quality_q48=");
|
||||
console_put_u64(slope_fit_quality_q48);
|
||||
console_println("");
|
||||
|
||||
console_puts("VM-PHYSICS: fleet_window_warm=");
|
||||
console_println(fleet_window.is_warm ? "YES" : "NO");
|
||||
|
||||
/* Per-VM breakdown (VM-FLEET-ATTRACTOR-DESIGN-20260705.md transparency
|
||||
* requirement): the aggregate above always reads Q.1 when conserved,
|
||||
* which says nothing about how heat is actually split among live VMs. */
|
||||
{
|
||||
vm_physics_node_t *n;
|
||||
for (n = vm_physics_head; n; n = n->next) {
|
||||
VMRegistryEntry entry;
|
||||
if (!n->physics.is_live) continue;
|
||||
|
||||
console_puts("VM-PHYSICS: vm_id=");
|
||||
{
|
||||
char uuid_buf[37];
|
||||
vm_uuid_format(n->vm_id, uuid_buf);
|
||||
console_puts(uuid_buf);
|
||||
}
|
||||
|
||||
if (capsule_vm_registry_get(n->vm_id, &entry) == 0) {
|
||||
console_puts(" name=");
|
||||
console_puts(entry.name);
|
||||
}
|
||||
|
||||
console_puts(" heat_q48=");
|
||||
console_put_u64(n->physics.execution_heat_q48);
|
||||
console_println("");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user