Stage C-a: work law — running VM charged at each of its own beats, with carry (FABRIC-3.8 §3.13.2)

vm_physics_charge(): per-beat slope into a per-VM carry, whole units pulled
from the rest of the live fleet in proportion to heat (conservative
vm_physics_transfer), rounding shortfall kept in the carry, no fleet_window
sample. Called from vm_tick() only for the switch-current VM. Closes G9:
a VM running its own mail on its own stack now heats up while it works.

Probe (reverted): baseline Hestia heat 0->0->0 over 71 beats (FAIL, proves
G9); with charge 0->11->23 at beats 15->51->86, identical on amd64/aarch64/
riscv64, conserved, PASS. Acceptance: dict_hash unchanged, WIREBIND relay 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 16:51:09 -04:00
co-authored by Claude Opus 5.5
parent 29447abb7d
commit 7c3736841d
19 changed files with 64261 additions and 6 deletions
@@ -63,6 +63,10 @@ typedef struct {
* overloading last_active_tick == 0, which is a
* legitimate tick value a first touch can land on */
int is_live;
uint64_t charge_carry; /* vm_physics_charge()'s owed transfer, Q48.16 --
* FABRIC-3.8.md §3.13.2: kept, never truncated
* away, so a one-beat charge (slope < 1 unit)
* still moves heat once enough beats accrue */
} VMPhysics;
typedef struct vm_physics_node {
@@ -203,6 +207,7 @@ void vm_physics_init(VMUuid vm_id)
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
node->physics.charge_carry = 0;
return;
}
@@ -214,6 +219,7 @@ void vm_physics_init(VMUuid vm_id)
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
node->physics.charge_carry = 0;
node->next = vm_physics_head;
vm_physics_head = node;
}
@@ -357,6 +363,52 @@ void vm_physics_touch(VMUuid vm_id)
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
}
/* FABRIC-3.8.md §3.13.2 (Stage C-a): see the header. Same fan-out shape as
* vm_physics_touch() above, but charged per beat of the running VM's own
* work rather than per dispatch, and with the sub-unit remainder carried
* instead of truncated away -- touch's elapsed_ticks * slope >> 16 is 0 for
* any single beat, which is why touch can't simply be called per beat. */
void vm_physics_charge(VMUuid vm_id)
{
vm_physics_node_t *target = vm_physics_find(vm_id);
vm_physics_node_t *n;
uint64_t amount, others_total = 0, moved_total, moved = 0;
if (!target || !target->physics.is_live) return;
target->physics.charge_carry += fleet_transfer_slope_q48;
amount = target->physics.charge_carry >> 16;
if (amount == 0) return;
target->physics.charge_carry &= 0xFFFFULL;
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
others_total += n->physics.execution_heat_q48;
}
}
/* Clamped to what the rest of the fleet holds, exactly like touch: a
* VM already holding all the heat has nothing left to pull, and that
* shortfall is dropped, not owed -- otherwise the carry would grow
* without bound while one VM runs alone. */
moved_total = (amount > others_total) ? others_total : amount;
if (moved_total == 0) return;
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);
moved += share;
}
}
/* The proportional split rounds each share down; what it failed to
* move is still owed, so it goes back into the carry rather than
* vanishing. Conservation is unaffected either way -- only what
* vm_physics_transfer() actually moved ever changed any heat. */
target->physics.charge_carry += (moved_total - moved) << 16;
}
/**
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
*
+12
View File
@@ -51,6 +51,7 @@
#include "../include/ssm_jacquard.h"
#include "vm_internal.h"
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- FABRIC-3.8.md §3.13.2 */
#include "starkernel/timer.h"
#include <errno.h>
@@ -140,6 +141,17 @@ void vm_tick(VM* vm)
* (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev k). */
vm_physics_heartbeat_tick(vm_monotonic_ns(vm));
/* FABRIC-3.8.md §3.13.2 (Stage C-a): the work law. This beat was
* produced by `vm`'s own execution; if `vm` is the VM physically
* running (switch-current), charge it one beat. A VM whose words run
* nested inside someone else's VM-EXEC is not switch-current and is
* not charged here -- that execution is already accounted by
* vm_physics_touch()'s dispatch law. Kernel-side only: nothing a VM
* executes can ask for, skip, or give back a charge. */
if (vm == sk_vm_switch_current_vm()) {
vm_physics_charge(vm->stadium_vm_id);
}
/* Plugin 2: System State Monitoring (Future) */
/* vm_tick_system_monitor(vm); */