/* 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.h - New, purpose-built "who runs next" signal * for preemptive context switching (FABRIC-3.md §XXVIII, Stage 3, * 2026-09-13). * * Deliberately NOT a repurposing of capsule_vm_physics.c's execution-heat * engine -- that measures word-level dispatch fairness over millions of * executions on a different timescale, and its own header explicitly * documents it as never touched from interrupt context (unlocked, by * design). This is a different physical quantity: instant-by-instant * run-readiness, consulted from real ISR context (heartbeat_tick()) every * timer tick. * * Concurrency discipline mirrors heartbeat.c's own §21.1-sanctioned * pattern for heartbeat_next_period_ns(): single-writer-ISR (tick()) / * single-reader-mainline (take_pending(), called from the cooperative * checkpoint in execute_colon_word()), no lock, because nothing on this * single hart is concurrent with the ISR while it runs. * * NOT truly interrupt-driven register/stack swapping (that was * considered and rejected for this stage -- see FABRIC-3.md §XXVIII * Stage 3 for why): the ISR only ever sets a flag. The actual switch * (Stage 2's already-proven sk_vm_context_switch()) happens later, at a * safe cooperative checkpoint on the mainline, once per word dispatch. * * Slot table is sized with headroom, not hardcoded to exactly today's 3 * participants (Hera/Hermes/Artemis) -- extending participation later * (Stage 4+) is another sk_vm_switch_signal_register() call, not a * redesign. * * FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2): the * slot table is no longer a fixed SK_SWITCH_MAX_SLOTS=16 compile-time array. * It is kmalloc'd at boot by sk_vm_switch_signal_boot_init(), sized from * stadium_max_vm_count() -- the same RAM-derived population bound Stadium * and session.c already use (session_boot_init() is the direct precedent * mirrored here). Every switch-signal participant is a Stadium VM, so * reusing that bound directly (rather than re-deriving a separate RAM * budget) needs no new sizing formula. */ #ifndef STARKERNEL_CAPSULE_VM_SWITCH_SIGNAL_H #define STARKERNEL_CAPSULE_VM_SWITCH_SIGNAL_H #ifdef __STARKERNEL__ #include #include "starkernel/vm_uuid.h" struct VM; /* Boot-time allocation (FABRIC-3.6.md task 3.1, 2026-09-21): kmalloc's the * slot table to stadium_max_vm_count() entries. Must run after * stadium_boot_init() (that bound is 0, and this fails, until Stadium has * computed it) and before the first sk_vm_switch_signal_register() call. * Soft failure -- returns -1 and leaves the table unallocated (capacity 0, * so register() below simply refuses every registration) rather than * halting boot, same posture as stadium_boot_init()/session_boot_init(). * Idempotent-unsafe: calling twice leaks the first allocation, so callers * must call it exactly once. */ int sk_vm_switch_signal_boot_init(void); /* Register a VM as a switch-signal participant. Returns its slot index, * or -1 if the slot table is full (or sk_vm_switch_signal_boot_init() was * never called / failed). Call once per participating VM, after that VM is * fully born (never mid-birth -- this stage has no critical-section * protection against being switched away mid-setup). FABRIC-3.8.md §3.6 * (Stage E): capsule_birth_baby() is now the one caller for every born VM * (Hera, never born through it, is registered in kernel_main.c). * Idempotent: registering an already-registered VM returns its slot. */ int sk_vm_switch_signal_register(VMUuid vm_id); /* Remove a switch-signal participant (FABRIC-3.md §XXVIII Stage 4, * 2026-09-14) -- Tripod VMs never need this (they live forever), but * WIREBIND-birthed identity VMs cycle through attach/detach repeatedly * and must free their slot for reuse, or the bounded table exhausts * after sk_vm_switch_signal_slot_capacity() attach/detach cycles. Compacts the table * (small, bounded, mutated only at attach/detach -- not a hot path). * Clears a pending switch targeting this VM, if any, so the checkpoint * never attempts to switch into a no-longer-registered participant. * No-op (returns -1) if vm_id was never registered. */ int sk_vm_switch_signal_unregister(VMUuid vm_id); /* * FABRIC-3.8.md §3.13 (Stage C-b, 2026-09-23): the ISR half of this module * (sk_vm_switch_signal_tick() from heartbeat_tick(), readiness accrual * against a fixed 50-tick threshold, a pending flag) is removed -- the * timer no longer decides anything. This module's own header comment above * (not a repurposing of VM-physics heat) is reversed by Captain Bob's * §3.11 ruling: the decision below IS a VM-physics heat comparison, made * workable at switch granularity by Stage C-a's per-beat charge. */ /* 1 if vm_id is a registered participant (the idle pump skips these: their * mail is delivered on their own stacks, by a switch). */ int sk_vm_switch_signal_is_participant(VMUuid vm_id); /* FABRIC-3.9.0.md §2 (Stage O-A, 2026-09-24): the observer role. The * Tripod (Hera, Artemis, Hestia) is marked by the kernel at registration -- * never by anything a VM executes. Observers are outside the compudynamic * comparison (never heat candidates, never charged, never in a floor) and * act on events: at a clean boundary an observer with mail (or, for Hera, * console input) goes ahead of every participant, and an observer acting * on an event is not displaced (ruling O-1). is_observer() is 0 for an * unregistered VM. */ void sk_vm_switch_signal_set_observer(VMUuid vm_id); int sk_vm_switch_signal_is_observer(VMUuid vm_id); /* A fresh, unique wait stamp (mainline, single writer). kernel_hermes.c * stamps each admitted message with one. */ uint64_t sk_vm_switch_signal_next_stamp(void); /* vm_id just stopped running (sk_vm_context_switch(), for `from`): record * when, for the "waiting longest" tie-break. */ void sk_vm_switch_signal_note_parked(VMUuid vm_id); /* Physical events after which the decision may differ: the running VM * produced a beat (vm_tick(), right after its Stage C-a charge), or mail * was admitted (sk_hermes_send_one()). take_decision_due() consumes the * flag at the next owned checkpoint. */ void sk_vm_switch_signal_note_beat(void); void sk_vm_switch_signal_note_mail(void); int sk_vm_switch_signal_take_decision_due(void); /* * sk_vm_switch_decide - who runs next, by physical comparison (§3.13.3). * * Among registered participants other than `self` that have work -- a * pending kernel-Hermes message, or parked on their own stack mid-task * (top frame not VM_FRAME_IDLE), or (Hera only) pending console input -- * returns the one with the lowest work heat (vm_physics_work_heat_of(): heat * earned by its own per-beat charges -- ruling C-4 (β), FABRIC-3.8.md * §3.13.7; Hera's structural endowment doesn't count); among equally cool * candidates, the one waiting longest (unique wait stamps). With * self_competes set (the caller still has work), returns a candidate only * if it is strictly cooler than `self`. Returns NULL when nobody should * run instead. A pure function of physical state: it keeps nothing * between calls, and its answer does not depend on table walk order. Not * a scheduler -- no queue, no priority list, nothing on a clock. */ struct VM *sk_vm_switch_decide(struct VM *self, int self_competes); /* Stage H, "floor on contact" (Captain Bob, 2026-09-23): vm_id just gained * work (kernel_hermes.c: its queue went from empty to one message). If it * had none before -- not running, not parked mid-task -- raise its work * heat to the lowest work heat among the VMs that have work now (the one * running and every eligible participant). A new or long-idle VM thus * joins the busy set level with its floor, rather than at 0 and running * alone until it "caught up" with VMs that have worked since boot. */ void sk_vm_switch_signal_note_work_gained(VMUuid vm_id); /* Reap one participant marked pending_reap (a WIREBIND identity whose * device detached while it was parked), if any. Call only at a safe point * (checkpoint, trampoline, idle point) -- same contract the old Stage 3 * checkpoint's inline reap had. */ void sk_vm_switch_reap_pending(void); /* Call once, immediately before a decided switch executes -- feeds the DoE * CSV counters below. */ void sk_vm_switch_signal_note_switch_performed(VMUuid target_id); /* DoE CSV read-only exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, * 2026-09-13). Stage C-b: ticks_since_switch now counts beats of the * running VM since the last switch (§3.13.4), not ISR ticks; the per-VM * readiness getters are gone with the readiness itself. */ uint64_t sk_vm_switch_signal_switch_count(void); /* cumulative, since boot */ uint32_t sk_vm_switch_signal_ticks_since_switch(void); int sk_vm_switch_signal_current_slot(void); /* -1 = none/unregistered */ int sk_vm_switch_signal_slot_count(void); /* FABRIC-3.6.md task 3.1 (2026-09-21): the table's boot-time-computed * capacity (0 if sk_vm_switch_signal_boot_init() was never called or * failed) -- the dynamic replacement for the old compile-time * SK_SWITCH_MAX_SLOTS=16. */ int sk_vm_switch_signal_slot_capacity(void); #endif /* __STARKERNEL__ */ #endif /* STARKERNEL_CAPSULE_VM_SWITCH_SIGNAL_H */