Rewired stadium_birth_hera() to admit unpinned then register through session_register()/session_set_pinned() instead of setting STADIUM_FLAG_PIN directly on the candidate header. Self-referential parent (vm_uuid_hera(), vm_uuid_hera()), matching capsule_run.h's parent_vm_id == vm_id root convention. Soft-fail, non-fatal, if session_register() fails -- Hera's actual Stadium admission is what the patron-zero invariant is about. Wired session_boot_init() into kernel_main.c right after stadium_boot_init(), before stadium_birth_hera(). Also converted §H.12's punch list from bold "DONE" markers to this document's established - [ ]/- [x] checkbox convention (already used throughout §A), for consistency. Verified 3-arch boot to ok> (amd64/aarch64/riscv64), no soft-fail message on any arch, Hermes/Artemis births unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
693 lines
27 KiB
C
693 lines
27 KiB
C
/*
|
||
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.
|
||
|
||
*/
|
||
|
||
/**
|
||
* stadium.c - The Stadium cell definitions (item 3.1) and boot-time
|
||
* allocation (item 3.2).
|
||
*
|
||
* Also the translation unit that makes stadium.h's compile-time assertions
|
||
* actually get compiled on all three architectures, not merely included by
|
||
* something that never builds.
|
||
*/
|
||
|
||
#include "starkernel/vm/stadium.h"
|
||
|
||
#ifdef __STARKERNEL__
|
||
|
||
#include "starkernel/kmalloc.h"
|
||
#include "starkernel/pmm.h"
|
||
#include "starkernel/console.h"
|
||
#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 */
|
||
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-3.md §H.12 step 4 */
|
||
|
||
static StadiumCell *stadium_cell_array = (StadiumCell *)0;
|
||
static uint8_t *stadium_bitmap = (uint8_t *)0;
|
||
static uint16_t *stadium_owner = (uint16_t *)0; /* widened from uint8_t
|
||
2026-08-15: the VM-count bound is no longer capped at
|
||
255 (see stadium_max_vm_count_val below), so a byte
|
||
could silently wrap. 65535 is ample headroom. */
|
||
static size_t stadium_ncells = 0;
|
||
static int stadium_initialized = 0;
|
||
static size_t stadium_max_vm_count_val = 0; /* computed at boot, see
|
||
stadium_boot_init() and stadium_max_vm_count() */
|
||
|
||
/* Sentinel for the header's `link` field while it is reused as a free-list
|
||
* next-pointer (item 3.7): link is uint32_t, but STADIUM_CELL_NONE is
|
||
* (size_t)-1 -- 64 bits wide on this target. Casting (size_t)-1 down to
|
||
* uint32_t truncates to the same bit pattern as this constant (safe), but
|
||
* casting THIS constant back up to size_t does not sign-extend to
|
||
* STADIUM_CELL_NONE (unsafe) -- hence the explicit link_to_size()/
|
||
* size_to_link() conversions below rather than a raw cast either direction. */
|
||
#define STADIUM_LINK_NONE ((uint32_t)-1)
|
||
|
||
static uint32_t size_to_link(size_t v) {
|
||
return (v == STADIUM_CELL_NONE) ? STADIUM_LINK_NONE : (uint32_t)v;
|
||
}
|
||
|
||
static size_t link_to_size(uint32_t v) {
|
||
return (v == STADIUM_LINK_NONE) ? STADIUM_CELL_NONE : (size_t)v;
|
||
}
|
||
|
||
/*
|
||
* StadiumVMQuota - one VM's ownership record (item 3.7, FABRIC.md §22.3).
|
||
* See stadium.h's stadium_admit() doc for why vm_id needs a linear search
|
||
* rather than direct indexing.
|
||
*/
|
||
typedef struct {
|
||
VMUuid vm_id;
|
||
int in_use;
|
||
size_t free_head;
|
||
uint64_t reservoir; /* item 4.1, FABRIC.md §17.7 -- Q48.16, heat this VM's
|
||
* quota holds but no resident patron has claimed.
|
||
* Invariant: Σ(resident patron heat) + reservoir ==
|
||
* Q48_ONE, checked the same way vm_physics_conserved()
|
||
* checks the fleet sum. */
|
||
} StadiumVMQuota;
|
||
|
||
/* kmalloc'd at stadium_boot_init() to stadium_max_vm_count_val entries,
|
||
* replacing the old static StadiumVMQuota stadium_quotas[STADIUM_MAX_VM_COUNT]
|
||
* (2026-08-15: the bound is computed from RAM, not a compile-time constant). */
|
||
static StadiumVMQuota *stadium_quotas = (StadiumVMQuota *)0;
|
||
|
||
/* Returns the quota slot index for vm_id, or -1 if none is granted. */
|
||
static int quota_slot_for_vm(VMUuid vm_id) {
|
||
size_t i;
|
||
for (i = 0; i < stadium_max_vm_count_val; i++) {
|
||
if (stadium_quotas[i].in_use && vm_uuid_equal(stadium_quotas[i].vm_id, vm_id)) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/* Freestanding: no libc printf. 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]);
|
||
}
|
||
|
||
int stadium_boot_init(void) {
|
||
/* Budgeted from kmalloc_get_stats(), not pmm_get_stats() (corrected
|
||
* 2026-08-15 -- see this function's doc in stadium.h): kmalloc_init()
|
||
* (M6) already carved its own fixed-size heap out of PMM before this
|
||
* ever runs, and every allocation below draws from that heap, not raw
|
||
* PMM, so PMM's own free-byte figure was the wrong pool to budget
|
||
* against. */
|
||
kmalloc_stats_t kstats = kmalloc_get_stats();
|
||
uint64_t budget_bytes = (kstats.free_bytes * (uint64_t)STADIUM_MEMORY_PERCENT) / 100u;
|
||
size_t ncells = (size_t)(budget_bytes / STADIUM_CELL_BYTES);
|
||
size_t bitmap_bytes = (ncells + 7u) / 8u;
|
||
|
||
if (ncells == 0) {
|
||
console_println("Stadium: boot-time allocation skipped (0 cells from memory budget)");
|
||
return -1;
|
||
}
|
||
|
||
StadiumCell *cells = (StadiumCell *)kmalloc(ncells * STADIUM_CELL_BYTES);
|
||
uint8_t *bitmap = (uint8_t *)kmalloc(bitmap_bytes);
|
||
uint16_t *owner = (uint16_t *)kmalloc(ncells * sizeof(uint16_t));
|
||
if (!cells || !bitmap || !owner) {
|
||
console_println("Stadium: kmalloc failed for boot-time allocation");
|
||
if (cells) kfree(cells);
|
||
if (bitmap) kfree(bitmap);
|
||
if (owner) kfree(owner);
|
||
return -1;
|
||
}
|
||
|
||
/* VM population bound (2026-08-15, replaces compile-time
|
||
* STADIUM_MAX_VM_COUNT): STADIUM_VM_MEMORY_PERCENT of whatever's left
|
||
* in the kmalloc heap AFTER the cell array above, divided by
|
||
* VM_MEMORY_SIZE (the real per-VM footprint -- 5 MiB, include/vm.h --
|
||
* not the small quota/word-slot bookkeeping tables). Floored to 1 so
|
||
* Hera can always boot; no ceiling otherwise (Captain Bob, 2026-08-15:
|
||
* the population is not knowable in advance). Clamped to what the
|
||
* uint16_t owner array can index, logged if that ever actually bites. */
|
||
kmalloc_stats_t kstats_after_cells = kmalloc_get_stats();
|
||
uint64_t vm_budget_bytes = (kstats_after_cells.free_bytes * (uint64_t)STADIUM_VM_MEMORY_PERCENT) / 100u;
|
||
size_t max_vm_count = (size_t)(vm_budget_bytes / VM_MEMORY_SIZE);
|
||
if (max_vm_count < 1u) max_vm_count = 1u;
|
||
if (max_vm_count > 65535u) {
|
||
console_println("Stadium: computed VM count exceeds owner-index range, clamped to 65535");
|
||
max_vm_count = 65535u;
|
||
}
|
||
|
||
StadiumVMQuota *quotas = (StadiumVMQuota *)kmalloc(max_vm_count * sizeof(StadiumVMQuota));
|
||
if (!quotas) {
|
||
console_println("Stadium: kmalloc failed for VM quota table");
|
||
kfree(cells);
|
||
kfree(bitmap);
|
||
kfree(owner);
|
||
return -1;
|
||
}
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)cells;
|
||
size_t n = ncells * STADIUM_CELL_BYTES;
|
||
size_t i;
|
||
for (i = 0; i < n; i++) raw[i] = 0;
|
||
}
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < bitmap_bytes; i++) bitmap[i] = 0;
|
||
}
|
||
|
||
/* Item 3.7: chain every cell into one free list, ascending index order
|
||
* (so the first-ever pop returns cell 0, per item 3.6), granted whole
|
||
* to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Reuses each
|
||
* cell's own `link` field as the next-free-cell pointer while
|
||
* unresident; see stadium_admit()'s doc for the scope of that reuse. */
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < ncells; i++) {
|
||
cells[i].header.link = size_to_link((i + 1 < ncells) ? (i + 1) : STADIUM_CELL_NONE);
|
||
cells[i].header.contains = STADIUM_CONTAINS_NONE;
|
||
owner[i] = 0;
|
||
}
|
||
}
|
||
{
|
||
size_t i;
|
||
for (i = 0; i < max_vm_count; i++) {
|
||
quotas[i].vm_id = vm_uuid_none();
|
||
quotas[i].in_use = 0;
|
||
quotas[i].free_head = STADIUM_CELL_NONE;
|
||
quotas[i].reservoir = 0;
|
||
}
|
||
}
|
||
quotas[0].vm_id = vm_uuid_hera();
|
||
quotas[0].in_use = 1;
|
||
quotas[0].free_head = 0;
|
||
/* item 4.1, §17.7: at quota-grant time, before any resident patron
|
||
* exists, the reservoir holds the VM's entire conserved share -- mirrors
|
||
* Hera holding the fleet's whole Q48_ONE before any other VM is born. */
|
||
quotas[0].reservoir = Q48_ONE;
|
||
|
||
stadium_cell_array = cells;
|
||
stadium_bitmap = bitmap;
|
||
stadium_owner = owner;
|
||
stadium_ncells = ncells;
|
||
stadium_quotas = quotas;
|
||
stadium_max_vm_count_val = max_vm_count;
|
||
stadium_initialized = 1;
|
||
|
||
console_puts("Stadium: ");
|
||
console_put_u64((uint64_t)ncells);
|
||
console_puts(" cells (");
|
||
console_put_u64((uint64_t)(ncells * STADIUM_CELL_BYTES) / 1024u);
|
||
console_puts(" KB), ");
|
||
console_put_u64((uint64_t)max_vm_count);
|
||
console_println(" VM slots");
|
||
|
||
return 0;
|
||
}
|
||
|
||
int stadium_is_initialized(void) {
|
||
return stadium_initialized;
|
||
}
|
||
|
||
size_t stadium_cell_count(void) {
|
||
return stadium_ncells;
|
||
}
|
||
|
||
size_t stadium_max_vm_count(void) {
|
||
return stadium_max_vm_count_val;
|
||
}
|
||
|
||
StadiumCell *stadium_cells(void) {
|
||
return stadium_cell_array;
|
||
}
|
||
|
||
uint8_t *stadium_header_bitmap(void) {
|
||
return stadium_bitmap;
|
||
}
|
||
|
||
/*
|
||
* Exhaustive switch, no default: §13/§18.3 require the behaviour set to be
|
||
* closed and fixed at build time. With -Wall -Werror, omitting a case here
|
||
* for a tag that exists is a build failure, not a silent gap -- the compiler
|
||
* enforces closedness, not just this comment.
|
||
*
|
||
* MIGRATE (blocks) is real, FABRIC-3.md §B: 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 (Hermes messages) needs no extra payload action here, confirmed
|
||
* 2026-08-26 (FABRIC-3.md §B): capsules/hermes/init.4th's MSG-ALLOC already
|
||
* admits every message with this behaviour, and MSG-FREE-NODE (called from
|
||
* both explicit ack and heat-driven reap) already evicts it -- has been
|
||
* real and live on every boot for weeks. The actual delivery action
|
||
* (VM-EXEC of the payload, MSG-DELIVER) already runs in FORTH, decoupled
|
||
* from and prior to Stadium reap; by the time this dispatches, delivery is
|
||
* already done. Was printed as "(stub)" for a long time regardless, which
|
||
* was simply wrong, not a real gap -- fixed to print the departing
|
||
* message's identity, same shape as MIGRATE's lbn print, instead of
|
||
* claiming to be unimplemented.
|
||
*
|
||
* COOL (words, item 4.1; VMs, FABRIC-3.md §B) needs no extra payload action
|
||
* here either, same reasoning as DELIVER: stadium_evict()'s own universal
|
||
* reservoir credit is the whole of what "cooling off the floor" means for
|
||
* both patron kinds -- real and live for both since item 4.1 and this
|
||
* session respectively. Fixed the same way, to print the departing
|
||
* patron's identity (word_id for a word, 0 -- the patron-zero convention --
|
||
* for a VM) instead of a misleading "(stub)" label.
|
||
*
|
||
* EXPIRE (ACL) alone remains genuinely unbuilt -- no ACL-Stadium
|
||
* integration of any kind exists yet (FABRIC-3.md §B).
|
||
*/
|
||
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: {
|
||
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: {
|
||
uint64_t msg_idx = stadium_cell_array[cell_index].header.identity;
|
||
console_puts("DELIVER msg_idx=");
|
||
console_put_u64(msg_idx);
|
||
console_println("");
|
||
break;
|
||
}
|
||
case STADIUM_BEHAVIOUR_EXPIRE:
|
||
console_println("EXPIRE (stub)");
|
||
break;
|
||
case STADIUM_BEHAVIOUR_COOL: {
|
||
uint64_t identity = stadium_cell_array[cell_index].header.identity;
|
||
console_puts("COOL identity=");
|
||
console_put_u64(identity);
|
||
console_println("");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* FABRIC.md §19.2/§19.3: density is heat / mass, read on demand from fields
|
||
* already in the header -- not a value a scheduler maintains. mass == 0
|
||
* (an empty or never-admitted slot; everything is zero-initialized until
|
||
* something is actually born into the Stadium, which nothing yet does)
|
||
* returns 0 rather than dividing by zero. An out-of-range cell_index also
|
||
* returns 0 -- there is no patron there to be dense.
|
||
*/
|
||
uint64_t stadium_density(size_t cell_index) {
|
||
StadiumPatronHeader *header;
|
||
|
||
if (cell_index >= stadium_ncells) return 0;
|
||
|
||
header = &stadium_cell_array[cell_index].header;
|
||
if (header->mass == 0) return 0;
|
||
|
||
return header->heat / (uint64_t)header->mass;
|
||
}
|
||
|
||
/* STADIUM_FLAG_PIN moved to stadium.h (FABRIC-3.md §H.12 step 3) -- see its
|
||
* doc comment there for why. */
|
||
|
||
static int bitmap_get(size_t cell_index) {
|
||
return (stadium_bitmap[cell_index / 8u] >> (cell_index % 8u)) & 1u;
|
||
}
|
||
|
||
static void bitmap_set(size_t cell_index) {
|
||
stadium_bitmap[cell_index / 8u] |= (uint8_t)(1u << (cell_index % 8u));
|
||
}
|
||
|
||
static void bitmap_clear(size_t cell_index) {
|
||
stadium_bitmap[cell_index / 8u] &= (uint8_t)~(1u << (cell_index % 8u));
|
||
}
|
||
|
||
/*
|
||
* FABRIC.md §17.2: reap means leaves the floor, not destroyed. Refuses a
|
||
* pinned header (§3) or one with a non-none `contains` (item 1.1: a patron
|
||
* holding another cannot be reaped). Refuses an out-of-range index or a cell
|
||
* whose discriminator bit is not set -- nothing resident there to reap.
|
||
*
|
||
* §20.5 #3 / item 3.6: panics, does not return, if a resident cell 0 (Hera,
|
||
* patron zero) is ever selected -- checked before pin/contains below, on
|
||
* purpose, so a wrongly-cleared pin cannot silently swallow the violation
|
||
* via the ordinary refusal path instead of surfacing it.
|
||
*/
|
||
int stadium_evict(size_t cell_index) {
|
||
StadiumPatronHeader *header;
|
||
uint16_t slot;
|
||
|
||
if (cell_index >= stadium_ncells) return -1;
|
||
if (!bitmap_get(cell_index)) return -1;
|
||
|
||
if (cell_index == STADIUM_HERA_CELL_INDEX) {
|
||
sk_hal_panic("Stadium: eviction selected patron zero (Hera) -- governor invariant broken");
|
||
}
|
||
|
||
header = &stadium_cell_array[cell_index].header;
|
||
if (header->flags & STADIUM_FLAG_PIN) return -1;
|
||
if (header->contains != STADIUM_CONTAINS_NONE) return -1;
|
||
|
||
stadium_dispatch(cell_index, (StadiumBehaviour)header->behaviour);
|
||
bitmap_clear(cell_index);
|
||
slot = stadium_owner[cell_index];
|
||
|
||
/* item 4.1, §17.7: the departing patron's remaining heat must flow back
|
||
* to its owner's reservoir before the cell returns to the free list, or
|
||
* every reap leaks heat and Σ(resident) + reservoir drifts below
|
||
* Q48_ONE. Captured BEFORE the zero-fill below, which would otherwise
|
||
* destroy it. */
|
||
stadium_quotas[slot].reservoir += header->heat;
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)header;
|
||
size_t i;
|
||
for (i = 0; i < sizeof(*header); i++) raw[i] = 0;
|
||
}
|
||
|
||
/* Item 3.7: return the freed cell to its owning VM's free list. contains
|
||
* must be set to the real "none" sentinel here, not left at the zero
|
||
* the fill above just wrote -- 0 is Hera's valid index, so a stray zero
|
||
* would make this freed cell look permanently "contains Hera" to the
|
||
* very next admit() that pops it. */
|
||
header->link = size_to_link(stadium_quotas[slot].free_head);
|
||
header->contains = STADIUM_CONTAINS_NONE;
|
||
stadium_quotas[slot].free_head = cell_index;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* FABRIC.md §19.3, §22.3, item 3.7: admit into vm_id's own quota. Pops that
|
||
* VM's free-list head first (O(1), no comparison needed -- §19.3's density
|
||
* rule only governs the full case). Only if that list is empty does this
|
||
* fall back to eviction, scoped to that SAME VM's own residents (quota
|
||
* isolation), finding the least-dense evictable one (pinned and
|
||
* contains-gated residents are skipped, never eviction candidates) and
|
||
* evicting it only if the candidate is strictly denser.
|
||
*/
|
||
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
|
||
int slot;
|
||
size_t i;
|
||
size_t idx;
|
||
size_t least_dense_index = STADIUM_CELL_NONE;
|
||
uint64_t least_dense_value = 0;
|
||
uint64_t candidate_density;
|
||
|
||
if (!stadium_initialized || !candidate) return STADIUM_CELL_NONE;
|
||
|
||
/* Multi-cell patrons need a continuation chain, and no header field is
|
||
* documented as carrying one's first index -- see this function's doc
|
||
* in stadium.h. Refuse rather than admit only the header and leak the
|
||
* rest, which would break capacity conservation. */
|
||
if (candidate->mass != 1) return STADIUM_CELL_NONE;
|
||
|
||
/* contains must be a real "none" or a real index -- catches garbage/
|
||
* uninitialized values, though not a zero-initialized candidate that
|
||
* meant "none": 0 is Hera's valid index, so that case is a caller
|
||
* contract issue this function cannot detect (see the header doc). */
|
||
if (candidate->contains != STADIUM_CONTAINS_NONE &&
|
||
candidate->contains >= stadium_ncells) return STADIUM_CELL_NONE;
|
||
|
||
slot = quota_slot_for_vm(vm_id);
|
||
if (slot < 0) return STADIUM_CELL_NONE;
|
||
|
||
if (stadium_quotas[slot].free_head != STADIUM_CELL_NONE) {
|
||
idx = stadium_quotas[slot].free_head;
|
||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||
stadium_cell_array[idx].header = *candidate;
|
||
bitmap_set(idx);
|
||
/* Item 4.2 fix (§25.7): record ownership so stadium_evict()'s
|
||
* reservoir credit and free-list return land on the VM that actually
|
||
* admitted this patron, not whatever owner[idx] held at boot. */
|
||
stadium_owner[idx] = (uint16_t)slot;
|
||
return idx;
|
||
}
|
||
|
||
for (i = 0; i < stadium_ncells; i++) {
|
||
StadiumPatronHeader *h;
|
||
|
||
if (!bitmap_get(i)) continue;
|
||
if (stadium_owner[i] != (uint16_t)slot) continue;
|
||
h = &stadium_cell_array[i].header;
|
||
if (h->flags & STADIUM_FLAG_PIN) continue;
|
||
if (h->contains != STADIUM_CONTAINS_NONE) continue;
|
||
|
||
{
|
||
uint64_t d = stadium_density(i);
|
||
if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) {
|
||
least_dense_index = i;
|
||
least_dense_value = d;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (least_dense_index == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
|
||
|
||
candidate_density = candidate->heat / (uint64_t)candidate->mass; /* mass == 1, guaranteed above */
|
||
if (candidate_density <= least_dense_value) return STADIUM_CELL_NONE;
|
||
|
||
if (stadium_evict(least_dense_index) != 0) return STADIUM_CELL_NONE;
|
||
|
||
/* stadium_evict() just pushed least_dense_index onto quotas[slot]'s free
|
||
* list -- its owner is `slot`, the same quota we scoped the search to.
|
||
* Single-threaded today (§21.1/§21.2: real concurrency is step-one, not
|
||
* yet built), so nothing else can have touched the list meanwhile; pop
|
||
* it straight back off. */
|
||
idx = stadium_quotas[slot].free_head;
|
||
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
|
||
stadium_cell_array[idx].header = *candidate;
|
||
bitmap_set(idx);
|
||
/* Same fix as the free-list-pop path above -- stadium_evict() just wrote
|
||
* owner[idx] = slot as part of reaping least_dense_index, so this is
|
||
* currently a no-op in practice, but it must not be assumed to stay a
|
||
* no-op: this is the correctness statement, not a redundant write. */
|
||
stadium_owner[idx] = (uint16_t)slot;
|
||
return idx;
|
||
}
|
||
|
||
/*
|
||
* FABRIC.md item 4.1a: one-time initial quota grant, not item 1.3's
|
||
* (still-unbuilt) recurring transfer. See stadium.h's doc for the full
|
||
* argument. Two passes over from_vm_id's free list: the first counts it
|
||
* (need the length before deciding where to split), the second detaches the
|
||
* first `half` cells into new_vm_id's own list, reassigning owner as it goes.
|
||
*/
|
||
int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
|
||
int from_slot, new_slot, i;
|
||
size_t count, half, idx, last_new, new_head, remainder_head;
|
||
|
||
if (!stadium_initialized) return -1;
|
||
if (quota_slot_for_vm(new_vm_id) >= 0) return -1;
|
||
|
||
from_slot = quota_slot_for_vm(from_vm_id);
|
||
if (from_slot < 0) return -1;
|
||
|
||
new_slot = -1;
|
||
for (i = 0; i < (int)stadium_max_vm_count_val; i++) {
|
||
if (!stadium_quotas[i].in_use) { new_slot = i; break; }
|
||
}
|
||
if (new_slot < 0) return -1;
|
||
|
||
count = 0;
|
||
idx = stadium_quotas[from_slot].free_head;
|
||
while (idx != STADIUM_CELL_NONE) {
|
||
count++;
|
||
idx = link_to_size(stadium_cell_array[idx].header.link);
|
||
}
|
||
|
||
half = count / 2;
|
||
if (half == 0) return -1; /* fewer than 2 free cells -- nothing to split */
|
||
|
||
new_head = stadium_quotas[from_slot].free_head;
|
||
idx = new_head;
|
||
last_new = STADIUM_CELL_NONE;
|
||
for (i = 0; i < (int)half; i++) {
|
||
stadium_owner[idx] = (uint16_t)new_slot;
|
||
last_new = idx;
|
||
idx = link_to_size(stadium_cell_array[idx].header.link);
|
||
}
|
||
/* idx now points to the first cell staying with from_slot (or
|
||
* STADIUM_CELL_NONE if half == count, i.e. an even list fully moved). */
|
||
remainder_head = idx;
|
||
stadium_cell_array[last_new].header.link = size_to_link(STADIUM_CELL_NONE);
|
||
stadium_quotas[from_slot].free_head = remainder_head;
|
||
|
||
stadium_quotas[new_slot].vm_id = new_vm_id;
|
||
stadium_quotas[new_slot].in_use = 1;
|
||
stadium_quotas[new_slot].free_head = new_head;
|
||
stadium_quotas[new_slot].reservoir = Q48_ONE;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* Shared by stadium_cell_heat_get()/_set(): resident AND owned by vm_id's
|
||
* own quota slot. Returns the quota slot on success, -1 on any refusal. */
|
||
static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
if (slot < 0) return -1;
|
||
if (cell_index >= stadium_ncells) return -1;
|
||
if (!bitmap_get(cell_index)) return -1;
|
||
if (stadium_owner[cell_index] != (uint16_t)slot) return -1;
|
||
return slot;
|
||
}
|
||
|
||
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
|
||
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
|
||
return stadium_cell_array[cell_index].header.heat;
|
||
}
|
||
|
||
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
|
||
int slot = owned_resident_slot(vm_id, cell_index);
|
||
uint64_t old_heat, delta, pulled;
|
||
|
||
if (slot < 0) return -1;
|
||
|
||
old_heat = stadium_cell_array[cell_index].header.heat;
|
||
if (new_heat == old_heat) return 0;
|
||
|
||
if (new_heat > old_heat) {
|
||
delta = new_heat - old_heat;
|
||
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
|
||
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
|
||
stadium_quotas[slot].reservoir -= pulled;
|
||
} else {
|
||
delta = old_heat - new_heat;
|
||
stadium_quotas[slot].reservoir += delta;
|
||
}
|
||
|
||
stadium_cell_array[cell_index].header.heat = new_heat;
|
||
return 0;
|
||
}
|
||
|
||
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
uint64_t pulled;
|
||
|
||
if (slot < 0) return 0;
|
||
|
||
pulled = (amount > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : amount;
|
||
stadium_quotas[slot].reservoir -= pulled;
|
||
return pulled;
|
||
}
|
||
|
||
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
|
||
if (slot < 0) return;
|
||
stadium_quotas[slot].reservoir += amount;
|
||
}
|
||
|
||
uint64_t stadium_reservoir_peek(VMUuid vm_id) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
|
||
if (slot < 0) return 0;
|
||
return stadium_quotas[slot].reservoir;
|
||
}
|
||
|
||
int stadium_quota_slot_for_vm(VMUuid vm_id) {
|
||
return quota_slot_for_vm(vm_id);
|
||
}
|
||
|
||
uint64_t stadium_resident_sum(VMUuid vm_id) {
|
||
int slot = quota_slot_for_vm(vm_id);
|
||
uint64_t sum = 0;
|
||
size_t i;
|
||
|
||
if (slot < 0) return 0;
|
||
|
||
for (i = 0; i < stadium_ncells; i++) {
|
||
if (!bitmap_get(i)) continue;
|
||
if (stadium_owner[i] != (uint16_t)slot) continue;
|
||
sum += stadium_cell_array[i].header.heat;
|
||
}
|
||
return sum;
|
||
}
|
||
|
||
/*
|
||
* FABRIC.md item 3.6 / item 4.1: see stadium.h's doc. Idempotent via the
|
||
* item-3.1 discriminator bitmap -- if cell 0 already reads as resident,
|
||
* something already birthed her (or, if it isn't actually Hera, something
|
||
* else already claimed cell 0 -- either way this must not clobber it).
|
||
*
|
||
* FABRIC-3.md §H.12 step 4: pinning no longer happens by setting
|
||
* STADIUM_FLAG_PIN on the candidate before admission -- session.c's
|
||
* session_set_pinned() is now the sole choke point for that bit (§H.10).
|
||
* Hera is admitted unpinned, then registered as session zero
|
||
* (self-referential parent, matching capsule_run.h's parent_vm_id ==
|
||
* vm_id convention for the root) and pinned through the session layer.
|
||
* Session registration/pinning is soft-fail (logged, not fatal) -- Hera's
|
||
* actual Stadium admission above already succeeded and is what the
|
||
* patron-zero invariant is about; session.c not yet being initialized
|
||
* must not break that.
|
||
*/
|
||
int stadium_birth_hera(void) {
|
||
StadiumPatronHeader candidate;
|
||
size_t idx;
|
||
|
||
if (!stadium_initialized) return -1;
|
||
if (stadium_ncells > 0 && bitmap_get(STADIUM_HERA_CELL_INDEX)) return 0;
|
||
|
||
{
|
||
uint8_t *raw = (uint8_t *)&candidate;
|
||
size_t i;
|
||
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
|
||
}
|
||
candidate.identity = 0;
|
||
candidate.heat = 0;
|
||
candidate.ttl = 0;
|
||
candidate.link = STADIUM_LINK_NONE;
|
||
candidate.contains = STADIUM_CONTAINS_NONE;
|
||
candidate.mass = 1;
|
||
candidate.flags = 0;
|
||
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
|
||
|
||
idx = stadium_admit(vm_uuid_hera(), &candidate);
|
||
if (idx == STADIUM_CELL_NONE) return -1; /* refused; should not happen (quota is fresh and empty) */
|
||
if (idx != STADIUM_HERA_CELL_INDEX) {
|
||
sk_hal_panic("Stadium: birth_hera did not land on cell 0 -- patron-zero invariant broken");
|
||
}
|
||
|
||
{
|
||
Session *s = session_register(vm_uuid_hera(), vm_uuid_hera(), "Hera");
|
||
if (s) {
|
||
s->stadium_cell = idx;
|
||
session_set_pinned(vm_uuid_hera(), 1);
|
||
} else {
|
||
console_println("Stadium: session_register failed for Hera (session.c not yet initialized?)");
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
#endif /* __STARKERNEL__ */ |