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:
co-authored by
Claude Opus 5.5
parent
29447abb7d
commit
7c3736841d
@@ -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
|
||||
*
|
||||
|
||||
@@ -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); */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user