Stadium: real block-patron admission + MIGRATE dispatch (FABRIC-3.md §B)

stadium_admit()'s mass==1 refusal looked like a hard blocker for 1024-byte
blocks, but stadium_word_dispatch()'s real candidate construction proves
Stadium cells carry pure identity/heat/bookkeeping, never the resident's
actual content -- a block patron follows the same shape (identity=LBN,
payload unused), so this was real, scoped work, not a case for stubbing.

New stadium_blocks.h/.c mirror stadium_words.c's admission/cooling shape,
keyed by (quota_slot, lbn) in a fixed-capacity open-addressing hash table
(tombstone deletion) instead of a dense array, since LBN space isn't
densely bounded like word_id. Wired into block_word_block()/buffer()/
update() (block_words.c), __STARKERNEL__-guarded. stadium_dispatch()'s
MIGRATE case now calls blk_flush(lbn) for real instead of printing
"(stub)". Three new Kconfig constants (STADIUM_BLOCK_HEAT_QUANTUM/
STADIUM_BLOCK_COOL_RATE_Q48/STADIUM_BLOCK_TRACK_CAP_MULT) mirror the
word-patron ones, same three-layer wiring.

VM-COOL/DELIVER/EXPIRE stay explicit punch-list items -- VM-COOL
deferred pending the still-iterating Tripod/Zuse/messaging vision,
DELIVER/EXPIRE are their own future subsystem integrations per
FABRIC.md's own "open, not resolved" notes.

Verified clean compile (zero warnings) and clean boot to REPL with
conservation intact (resident_sum + reservoir == Q48_ONE) on all three
architectures (amd64/aarch64/riscv64); BLOCK/BUFFER touches exercised
live from the REPL with no crash; a 22,000-distinct-block flood loop
against an artificially shrunk Stadium ran clean under heavy admission
load. A live MIGRATE console fire was not directly observed this
session (root-caused to a pre-existing reservoir-floor/density-eviction
interaction unrelated to this change, documented in FABRIC-3.md) --
flagged as an honest follow-up, not silently claimed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
This commit is contained in:
Robert Allan James
2026-08-25 23:28:59 -04:00
co-authored by Claude Sonnet 5
parent ecdb547786
commit c7c9332321
17 changed files with 27731 additions and 13 deletions
+2
View File
@@ -56,6 +56,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#include "starkernel/vm/parity.h"
#include "starkernel/vm/stadium.h"
#include "starkernel/vm/stadium_words.h"
#include "starkernel/vm/stadium_blocks.h"
#include "starkernel/capsule_generated.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/capsule_birth.h" /* capsule_birth_mama, capsule_find_mama_init */
@@ -538,6 +539,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
* words. */
(void)stadium_birth_hera();
stadium_words_init();
stadium_blocks_init(); /* FABRIC-3.md §B: block-patron layer, same ordering as words */
/* M7: VM Bootstrap and Parity Validation */
console_println("VM: bootstrap parity...");
+14 -5
View File
@@ -40,6 +40,7 @@
#include "starkernel/hal/hal.h"
#include "starkernel/q48_16.h" /* Q48_ONE -- item 4.1's reservoir starts each VM's quota at 1.0 */
#include "vm.h" /* VM_MEMORY_SIZE -- the per-VM footprint stadium_max_vm_count() budgets against */
#include "block_subsystem.h" /* blk_flush() -- STADIUM_BEHAVIOUR_MIGRATE's real write-back action */
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
static uint8_t *stadium_bitmap = (uint8_t *)0;
@@ -255,18 +256,26 @@ uint8_t *stadium_header_bitmap(void) {
* for a tag that exists is a build failure, not a silent gap -- the compiler
* enforces closedness, not just this comment.
*
* Handlers are stubs: the real actions belong to subsystems not yet migrated
* onto the Stadium (Phase 4, §25.5). Nothing calls this yet either -- item
* 3.5 is the first consumer.
* MIGRATE (blocks) is real, FABRIC-3.md §B (MIGRATE punch-list item):
* write-back via blk_flush(), called with the departing patron's identity
* (the LBN) -- stadium_evict() calls this BEFORE zeroing the header, so
* `identity` is still valid here. DELIVER/EXPIRE/VM-COOL remain stubs --
* their own subsystems (Hermes, ACL, Tripod VM lifecycle) have not been
* migrated onto the Stadium yet, each its own future pass.
*/
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour) {
console_puts("Stadium: dispatch cell=");
console_put_u64((uint64_t)cell_index);
console_puts(" behaviour=");
switch (behaviour) {
case STADIUM_BEHAVIOUR_MIGRATE:
console_println("MIGRATE (stub)");
case STADIUM_BEHAVIOUR_MIGRATE: {
uint32_t lbn = (uint32_t)stadium_cell_array[cell_index].header.identity;
console_puts("MIGRATE lbn=");
console_put_u64((uint64_t)lbn);
console_println("");
blk_flush(lbn);
break;
}
case STADIUM_BEHAVIOUR_DELIVER:
console_println("DELIVER (stub)");
break;
+352
View File
@@ -0,0 +1,352 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.
*/
/**
* stadium_blocks.c - Block patrons on the Stadium (FABRIC-3.md §B/§D,
* MIGRATE punch-list item). See stadium_blocks.h for the design rationale.
*/
#include "starkernel/vm/stadium_blocks.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium.h"
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE */
#include "starforth_config.h" /* STADIUM_BLOCK_HEAT_QUANTUM, STADIUM_BLOCK_COOL_RATE_Q48,
STADIUM_BLOCK_TRACK_CAP_MULT */
/*
* StadiumBlockSlot - one entry in the fixed-capacity open-addressing hash
* table. Keyed by (quota_slot, lbn) together -- LBN numbering is global
* across VMs (unlike word_id), but two VMs touching the SAME lbn must still
* land in different Stadium cells under different quotas, so both fields
* are part of the key (same lesson stadium_words.c's item 4.2 fix already
* applied for word_id).
*
* `state` is three-valued, not a bool: standard open-addressing deletion
* needs EMPTY vs. TOMBSTONE distinguished from OCCUPIED, or reclaiming a
* stale entry's slot (self-healing, see stadium_word_dispatch()'s identical
* problem for the dense word map) would silently break the probe chain for
* any other key that happened to hash into the same bucket and probed past
* this slot to find its own home.
*/
typedef enum {
BLOCK_SLOT_EMPTY = 0,
BLOCK_SLOT_OCCUPIED,
BLOCK_SLOT_TOMBSTONE
} StadiumBlockSlotState;
typedef struct {
uint8_t state;
int16_t quota_slot;
uint32_t lbn;
size_t cell_index;
uint64_t last_decay_tick;
} StadiumBlockSlot;
static StadiumBlockSlot *block_slots = (StadiumBlockSlot *)0;
static size_t block_slots_cap = 0;
static int blocks_initialized = 0;
/* Per-quota-slot counters, kmalloc'd to stadium_max_vm_count() entries --
* same reasoning as stadium_words.c's stat_promotions/stat_evictions: a
* single global counter can't answer "how is vm_id's own table doing"
* once more than one VM holds a quota. */
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
void stadium_blocks_init(void) {
size_t vm_count;
size_t cell_count;
size_t i;
if (blocks_initialized) return; /* not re-entrant -- see stadium_blocks.h */
cell_count = stadium_cell_count();
vm_count = stadium_max_vm_count();
if (cell_count == 0 || vm_count == 0) {
console_println("Stadium blocks: init skipped (Stadium not initialized)");
return;
}
block_slots_cap = cell_count * (size_t)STADIUM_BLOCK_TRACK_CAP_MULT;
block_slots = (StadiumBlockSlot *)kmalloc(block_slots_cap * sizeof(StadiumBlockSlot));
stat_promotions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
if (!block_slots || !stat_promotions || !stat_evictions) {
console_println("Stadium blocks: kmalloc failed for tracking tables");
if (block_slots) kfree(block_slots);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
block_slots = (StadiumBlockSlot *)0;
block_slots_cap = 0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < block_slots_cap; i++) {
block_slots[i].state = BLOCK_SLOT_EMPTY;
block_slots[i].quota_slot = -1;
block_slots[i].lbn = 0;
block_slots[i].cell_index = STADIUM_CELL_NONE;
block_slots[i].last_decay_tick = 0;
}
for (i = 0; i < vm_count; i++) {
stat_promotions[i] = 0;
stat_evictions[i] = 0;
}
blocks_initialized = 1;
}
static int cell_is_resident(size_t idx) {
const uint8_t *bm = stadium_header_bitmap();
if (!bm) return 0;
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* block_hash - Multiplicative mix of (quota_slot, lbn) into [0, block_slots_cap).
* No libc hash dependency (freestanding kernel); Knuth's multiplicative
* method (constant 2654435761, the standard 32-bit golden-ratio prime) is
* enough to spread a monotonically-touched LBN sequence across buckets --
* this table's job is short probe chains under normal access patterns, not
* cryptographic distribution.
*/
static size_t block_hash(int16_t quota_slot, uint32_t lbn) {
uint32_t key = lbn ^ ((uint32_t)(uint16_t)quota_slot << 16) ^ ((uint32_t)(uint16_t)quota_slot);
key = key * 2654435761u;
return (size_t)(key % (uint32_t)block_slots_cap);
}
/*
* table_probe - Single linear-probe pass serving both lookup and insertion.
* Walks at most block_slots_cap slots starting at the key's hash bucket.
*
* - If an OCCUPIED slot matching (quota_slot, lbn) is found: returns its
* index, *out_found = 1.
* - Else, if any EMPTY or TOMBSTONE slot was seen along the way: returns
* the FIRST such slot's index (so insertion reuses the earliest opening,
* standard open-addressing practice), *out_found = 0.
* - Else (table genuinely full, no opening and no match): returns
* block_slots_cap, *out_found = 0.
*
* Probing must continue past TOMBSTONE slots when searching for a match --
* a tombstone marks "something used to live here," not "the chain ends
* here," or a live entry further down the same chain would become
* unreachable.
*/
static size_t table_probe(int16_t quota_slot, uint32_t lbn, int *out_found) {
size_t start = block_hash(quota_slot, lbn);
size_t first_open = block_slots_cap; /* sentinel: none seen yet */
size_t i;
for (i = 0; i < block_slots_cap; i++) {
size_t idx = (start + i) % block_slots_cap;
StadiumBlockSlot *s = &block_slots[idx];
if (s->state == BLOCK_SLOT_EMPTY) {
if (first_open == block_slots_cap) first_open = idx;
break; /* empty slot ends any possible chain for this key */
}
if (s->state == BLOCK_SLOT_TOMBSTONE) {
if (first_open == block_slots_cap) first_open = idx;
continue;
}
/* OCCUPIED */
if (s->quota_slot == quota_slot && s->lbn == lbn) {
*out_found = 1;
return idx;
}
}
*out_found = 0;
return first_open; /* block_slots_cap if truly full */
}
/*
* block_dispatch_pull - Reservoir pull for block-touch admission, same
* Q48_ONE / 3 floor stadium_word_dispatch()'s word_dispatch_pull() enforces
* and for the identical reason (FABRIC.md §25.5/§25.7): block-touch
* admission alone must never be able to drain a VM's reservoir below the
* floor other reservoir consumers (word execution, application-level
* pulls) depend on. Own copy, not shared code across files -- same
* decision stadium_words.c's own version already made.
*/
static uint64_t block_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks) {
int slot;
int found;
size_t idx;
int stale = 0;
if (!blocks_initialized) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
idx = table_probe((int16_t)slot, lbn, &found);
if (found) {
StadiumBlockSlot *bs = &block_slots[idx];
size_t cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) {
stale = 1;
} else if (stadium_cells()[cell].header.identity != (uint64_t)lbn) {
stale = 1;
}
if (!stale) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - bs->last_decay_tick;
if (elapsed > 0) {
uint64_t per_tick = (h->heat * (uint64_t)STADIUM_BLOCK_COOL_RATE_Q48) >> 16;
uint64_t cooled = per_tick * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
bs->last_decay_tick = heartbeat_ticks;
}
h->heat += block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
return;
}
/* Stale: reclaim this exact slot -- idx is still this key's home. */
bs->state = BLOCK_SLOT_TOMBSTONE;
stat_evictions[slot]++;
}
if (idx >= block_slots_cap) return; /* table full: skip tracking this touch */
{
uint64_t pulled = block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
size_t cell_idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)lbn;
candidate.heat = pulled;
candidate.ttl = 0;
candidate.link = 0; /* unused for block patrons, same as words */
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0; /* unpinned */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_MIGRATE;
cell_idx = stadium_admit(vm_id, &candidate);
if (cell_idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
return;
}
block_slots[idx].state = BLOCK_SLOT_OCCUPIED;
block_slots[idx].quota_slot = (int16_t)slot;
block_slots[idx].lbn = lbn;
block_slots[idx].cell_index = cell_idx;
block_slots[idx].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
/* Freestanding: no libc printf. Same small utility stadium_words.c
* duplicates locally (and stadium.c before it) -- prints an unsigned
* decimal, no leading zeros. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) {
int slot = stadium_quota_slot_for_vm(vm_id);
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum = 0;
uint64_t reservoir;
size_t i;
if (!blocks_initialized || slot < 0) {
console_println("Stadium blocks: not initialized or vm_id holds no quota");
return;
}
promotions = stat_promotions[slot];
evictions = stat_evictions[slot];
/* Block-only resident sum: walk this VM's own table entries, same
* self-healing residency check stadium_block_dispatch() uses -- a
* stale entry contributes 0, not garbage. Not exposed as a separate
* API (unlike stadium_words_resident_heat()) since nothing else needs
* it yet; add one if a caller shows up. */
for (i = 0; i < block_slots_cap; i++) {
StadiumBlockSlot *bs = &block_slots[i];
size_t cell;
if (bs->state != BLOCK_SLOT_OCCUPIED || bs->quota_slot != (int16_t)slot) continue;
cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) continue;
if (stadium_cells()[cell].header.identity != (uint64_t)bs->lbn) continue;
resident_sum += stadium_cells()[cell].header.heat;
}
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium blocks: promotions=");
console_put_u64(promotions);
console_puts(" evictions=");
console_put_u64(evictions);
console_println("");
console_puts("Stadium blocks resident_sum=");
console_put_u64(resident_sum);
console_puts(" reservoir=");
console_put_u64(reservoir);
console_println(" (block-only term, not a standalone Q48_ONE check -- reservoir is shared)");
}
#endif /* __STARKERNEL__ */
+13
View File
@@ -57,6 +57,10 @@
#include <string.h>
#include <stdio.h>
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium_blocks.h" /* stadium_block_dispatch() -- FABRIC-3.md §B */
#endif
/* ----------------------------------------------------------------------
* Architecture:
* - Layer 1: blkio (vtable abstraction)
@@ -305,6 +309,9 @@ void block_word_block(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
vaddr_t vaddr = blk_vm_load(vm, (uint32_t) blk, 0);
if (!vaddr) { vm->error = 1; return; }
set_scr(vm, blk);
@@ -316,6 +323,9 @@ void block_word_buffer(VM *vm) {
if (vm->dsp < 0) { vm->error = 1; return; }
cell_t blk = vm_pop(vm);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
vaddr_t vaddr = blk_vm_assign(vm, (uint32_t) blk);
if (!vaddr) { vm->error = 1; return; }
set_scr(vm, blk);
@@ -328,6 +338,9 @@ void block_word_buffer(VM *vm) {
void block_word_update(VM *vm) {
cell_t blk = vm_load_cell(vm, vm->scr_addr);
if (blk == 0 || !blk_is_valid((uint32_t) blk)) { vm->error = 1; return; }
#ifdef __STARKERNEL__
stadium_block_dispatch(vm->stadium_vm_id, (uint32_t) blk, vm->heartbeat.tick_count);
#endif
int s = blk_vm_find(vm, (uint32_t) blk);
if (s < 0) { vm->error = 1; return; }
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)s * BLOCK_SIZE;