Stage 3: timer-driven preemptive switching, live on all 3 arches (FABRIC-3.md §XXVIII)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Fourth stage of the preemptive context-switching plan, and the biggest.
LithosAnanke now genuinely, continuously preempts between Hera, Hermes,
and Artemis -- timer-driven, running live for the entire remainder of
every boot once the Tripod fleet registers, not a bounded probe.

A real design fork was resolved before writing code: the naive approach
(the timer ISR calling Stage 2's sk_vm_context_switch() directly) is
broken -- Stage 0's trap frame lives on whatever stack was active at
interrupt time, and jumping to a different stack via Stage 2's own
independent swap mid-handler would abandon that trap frame unresumed,
guaranteed corruption on the first tick. Chose the safer of two named
options: the ISR only ever sets a flag and returns completely normally
through its own full epilogue; the actual switch happens moments later,
via Stage 2's already-proven mechanism, at a safe cooperative checkpoint
on the mainline (execute_colon_word()'s per-word dispatch loop, checked
on literally every word, not throttled to the existing 256-word
heartbeat-tuning cadence) -- confirmed with the user that word-level
granularity is fine-grained enough given the eventual Zynq FPGA target
where a word is a mnemonic.

New capsule_vm_switch_signal.c/.h: a purpose-built run-readiness signal,
deliberately separate from capsule_vm_physics.c's execution-heat engine
(that one's own header documents itself as never touched from interrupt
context, by design). Slot table sized with headroom (8) rather than
hardcoded to today's 3 participants, so extending participation later is
another register() call, not a redesign -- per direct request to leave
room for swapping the participant set. Simple linear accumulate-then-
threshold for this first cut; a fancier law can replace it later without
touching the mechanism around it. heartbeat_tick() gains its one
deliberate, documented amendment to this file's own top-half/bottom-half
discipline -- the first time this codebase reaches into VM-scheduling
state from real ISR context.

Registration happens only after all three VMs are fully born, right
before the REPL starts -- no critical-section protection yet against
being switched away mid-birth-setup.

Known, flagged rough edge (not reconciled this pass): MSG-TICK's own
idle-pump and this new mechanism can still independently move control
between the same VMs; not observed to interact badly in verification,
but not fully unified either.

Verified interactively at the console on all 3 architectures with
continuous background preemption running throughout -- amd64 computed
`1 1 + .` -> 2, aarch64 computed `1 1 + dup DUP * . CR` -> 4, both
correct, REPL fully responsive, zero fault indicators over sustained
runtime.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S
This commit is contained in:
Robert Allan James
2026-09-13 19:32:36 -04:00
co-authored by Claude Sonnet 5
parent f790d0995e
commit 986d042aa7
14 changed files with 27452 additions and 1 deletions
@@ -0,0 +1,109 @@
/*
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/log_attrib.h" /* vm_log_attributed_vm() */
#include <stddef.h>
/* Headroom beyond today's 3 participants (Hera/Hermes/Artemis) -- see
* the header's own doc comment on why this isn't hardcoded to 3. */
#define SK_SWITCH_MAX_SLOTS 8
/* 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;
} 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;
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;
return g_slot_count++;
}
void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
current = vm_log_attributed_vm();
if (!current) return; /* nothing interpreting right now (boot/driver/HAL context) */
current_slot = slot_for_vm_id(current->stadium_vm_id);
if (current_slot < 0) return; /* current VM isn't a registered participant */
for (i = 0; i < g_slot_count; i++) {
if (i == current_slot) {
g_slots[i].readiness = 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_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;
}
#endif /* __STARKERNEL__ */