/* 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. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * capsule_vm_switch_signal.c - see the header for the full contract * (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13). */ #ifdef __STARKERNEL__ #include "starkernel/capsule_vm_switch_signal.h" #include "vm.h" #include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */ #include /* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): bumped from 8 to 16 to admit * WIREBIND identity VMs alongside the fixed Tripod fleet. 16 is not a new * guess -- it matches messaging.4th's own VM-MAX (the real, already- * agreed system-wide ceiling: 3 permanent Tripod slots + 13 for * identities, "identities get NEW slots 3-10, never renumbered" plus * headroom to VM-MAX itself). See feedback_no_hardcoded_small_scale_bounds * in project memory -- an invented cap here was rejected before. */ #define SK_SWITCH_MAX_SLOTS 16 /* Ticks a non-running participant must accumulate readiness before a * switch to it is requested. Simple linear accumulate-then-threshold, * not the fancier relaxation law a later pass might want -- correctness * and a clean single-writer/single-reader story mattered more than the * exact law for this first cut (this stage's own design-review latitude, * per the plan). Reset to 0 the instant a slot becomes current, so a * VM that just ran needs a full threshold's worth of ticks idle before * it can be selected again -- a crude but real fairness property. */ #define SK_SWITCH_READINESS_THRESHOLD 50u typedef struct { VMUuid vm_id; uint32_t readiness; int has_work; /* Stage 3 follow-on (2026-09-14): set by * sk_vm_switch_signal_mark_work() (mainline, * MSG-SEND hook), cleared here in tick() the * instant this slot becomes current -- see that * function's own doc comment in the header. */ } sk_switch_slot_entry_t; static sk_switch_slot_entry_t g_slots[SK_SWITCH_MAX_SLOTS]; static int g_slot_count; static int g_pending; static VMUuid g_pending_target; /* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */ static int g_current_slot_cached = -1; static uint64_t g_switch_count; static uint32_t g_ticks_since_switch; static int slot_for_vm_id(VMUuid vm_id) { int i; for (i = 0; i < g_slot_count; i++) { if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i; } return -1; } int sk_vm_switch_signal_register(VMUuid vm_id) { if (g_slot_count >= SK_SWITCH_MAX_SLOTS) return -1; g_slots[g_slot_count].vm_id = vm_id; g_slots[g_slot_count].readiness = 0; g_slots[g_slot_count].has_work = 0; return g_slot_count++; } int sk_vm_switch_signal_unregister(VMUuid vm_id) { int slot = slot_for_vm_id(vm_id); if (slot < 0) return -1; for (int i = slot; i < g_slot_count - 1; i++) { g_slots[i] = g_slots[i + 1]; } g_slot_count--; /* A pending switch targeting this VM must not survive its removal -- * the checkpoint in execute_colon_word() would otherwise try to * switch into a VM no longer tracked here. */ if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) { g_pending = 0; } /* g_current_slot_cached is a DoE-observable value only (see its own * declaration comment); if compaction just invalidated it, the very * next tick() recomputes it from scratch via slot_for_vm_id(), so a * momentarily-stale cache here is harmless. */ return 0; } void sk_vm_switch_signal_tick(void) { VM *current; int current_slot, i; /* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was * vm_log_attributed_vm() -- wrong source. That tracks nested * vm_interpret() calls for log attribution and has no way to know a * VM is running while parked inside switch.c's own raw-C trampoline * (sk_vm_switch_entry()), which never itself calls vm_interpret(). A * VM that only ever ran the trampoline was therefore never seen as * "current," so its own readiness/has_work below never reset -- * tick() re-armed the very next timer tick, forever, once nothing * else happened to be independently flipping attribution around it * (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_ * current_vm() is authoritative instead: the switch mechanism itself * updates it on every switch, so it reflects who is physically * running regardless of whether that VM ever calls vm_interpret(). */ current = sk_vm_switch_current_vm(); if (!current) return; /* boot: no switch has happened yet, not seeded */ current_slot = slot_for_vm_id(current->stadium_vm_id); if (current_slot < 0) return; /* current VM isn't a registered participant */ g_current_slot_cached = current_slot; g_ticks_since_switch++; for (i = 0; i < g_slot_count; i++) { if (i == current_slot) { g_slots[i].readiness = 0; g_slots[i].has_work = 0; } else { g_slots[i].readiness++; } } if (!g_pending) { for (i = 0; i < g_slot_count; i++) { if (i != current_slot && g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD && g_slots[i].has_work) { g_pending = 1; g_pending_target = g_slots[i].vm_id; break; } } } } VMUuid sk_vm_switch_signal_take_pending(void) { if (!g_pending) return vm_uuid_none(); g_pending = 0; return g_pending_target; } void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) { /* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): * reset the target's own readiness/has_work HERE, deterministically, * at the switch decision point -- not by waiting for tick()'s * current-slot polling to observe it running. That polling depends on * sk_vm_switch_current_vm() catching a real ISR tick during whatever * window the target is genuinely "current" for, and for a VM whose * only production behavior is switch.c's placeholder trampoline (an * immediate yield-back), that window can be as small as a few * instructions -- too narrow for a timer tick to reliably land in. * Without this, the target's eligibility never clears, tick() re-arms * on the very next timer tick, and the result is a continuous switch * storm (found live: 101% CPU, zero forward progress). This makes * correctness independent of ISR timing; current_slot (via * sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable * value and for excluding the running slot from readiness accrual, * but is no longer load-bearing for storm prevention. */ int slot = slot_for_vm_id(target_id); if (slot >= 0) { g_slots[slot].readiness = 0; g_slots[slot].has_work = 0; } g_switch_count++; g_ticks_since_switch = 0; } void sk_vm_switch_signal_mark_work(VMUuid vm_id) { int slot = slot_for_vm_id(vm_id); if (slot < 0) return; g_slots[slot].has_work = 1; } uint64_t sk_vm_switch_signal_switch_count(void) { return g_switch_count; } uint32_t sk_vm_switch_signal_ticks_since_switch(void) { return g_ticks_since_switch; } int sk_vm_switch_signal_current_slot(void) { return g_current_slot_cached; } int sk_vm_switch_signal_slot_count(void) { return g_slot_count; } uint32_t sk_vm_switch_signal_readiness(int slot) { if (slot < 0 || slot >= g_slot_count) return 0; return g_slots[slot].readiness; } uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) { int slot = slot_for_vm_id(vm_id); if (slot < 0) return 0; return g_slots[slot].readiness; } #endif /* __STARKERNEL__ */