/* 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-2.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-0.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; size_t free_count; /* FABRIC-3.md SXVI donor-floor fix: O(1)-maintained * length of this VM's own free list -- lets * stadium_grant_quota()'s caller pick whichever VM * actually has the most spare capacity to split * from, instead of always Hera specifically (the * old policy silently starved new VM births once * HERA's own list ran thin, even while other VMs * granted a generous initial half sat on nearly all * of it unused). Kept in exact lockstep with every * free_head push/pop below and in stadium_evict()/ * stadium_admit(). */ size_t resident_head; /* FABRIC-3.md aarch64 Stadium/COOL scaling fix: * head of this VM's own resident-cell list, threaded * through stadium_resident_next[]/stadium_resident_prev[] * (global, indexed by cell, below). Lets * stadium_admit()'s eviction-fallback scan walk only * this VM's own residents -- what its doc already * claimed ("scoped to that SAME VM's own resident * patrons only") -- instead of filtering the entire * stadium_ncells array by owner, which is what the * scan actually did before this fix. STADIUM_CELL_NONE * when empty. */ uint64_t reservoir; /* item 4.1, FABRIC-0.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; /* Doubly-linked resident-list threading, one entry per cell, parallel to * stadium_owner[] -- kmalloc'd at stadium_boot_init() to stadium_ncells * entries each. Only meaningful for cells currently resident (bitmap bit * set); STADIUM_LINK_NONE-equivalent (via link_to_size()/size_to_link(), * defined below) terminates each end. Doubly-linked because stadium_evict() * removes by cell_index directly (not by list-walk), so O(1) unlink needs a * prev pointer, not just next. */ static uint32_t *stadium_resident_next = (uint32_t *)0; static uint32_t *stadium_resident_prev = (uint32_t *)0; /* resident_list_push - O(1) insert cell_index at the front of slot's own * resident list. Caller must have already set bitmap/owner for cell_index. */ static void resident_list_push(int slot, size_t cell_index) { size_t old_head = stadium_quotas[slot].resident_head; stadium_resident_prev[cell_index] = size_to_link(STADIUM_CELL_NONE); stadium_resident_next[cell_index] = size_to_link(old_head); if (old_head != STADIUM_CELL_NONE) { stadium_resident_prev[old_head] = size_to_link(cell_index); } stadium_quotas[slot].resident_head = cell_index; } /* resident_list_remove - O(1) splice cell_index out of slot's own resident * list. Caller must call this BEFORE clearing cell_index's bitmap bit. */ static void resident_list_remove(int slot, size_t cell_index) { size_t prev = link_to_size(stadium_resident_prev[cell_index]); size_t next = link_to_size(stadium_resident_next[cell_index]); if (prev != STADIUM_CELL_NONE) { stadium_resident_next[prev] = size_to_link(next); } else { stadium_quotas[slot].resident_head = next; } if (next != STADIUM_CELL_NONE) { stadium_resident_prev[next] = size_to_link(prev); } } /* 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)); uint32_t *resident_next = (uint32_t *)kmalloc(ncells * sizeof(uint32_t)); uint32_t *resident_prev = (uint32_t *)kmalloc(ncells * sizeof(uint32_t)); if (!cells || !bitmap || !owner || !resident_next || !resident_prev) { console_println("Stadium: kmalloc failed for boot-time allocation"); if (cells) kfree(cells); if (bitmap) kfree(bitmap); if (owner) kfree(owner); if (resident_next) kfree(resident_next); if (resident_prev) kfree(resident_prev); 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); kfree(resident_next); kfree(resident_prev); 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; /* Not on any resident list yet -- nothing is resident until * stadium_admit() puts it there. */ resident_next[i] = size_to_link(STADIUM_CELL_NONE); resident_prev[i] = size_to_link(STADIUM_CELL_NONE); } } { 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].free_count = 0; quotas[i].resident_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; quotas[0].free_count = ncells; /* 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_resident_next = resident_next; stadium_resident_prev = resident_prev; 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-2.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-2.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-2.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-2.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-0.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-2.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-0.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); slot = stadium_owner[cell_index]; resident_list_remove((int)slot, cell_index); bitmap_clear(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; stadium_quotas[slot].free_count++; return 0; } /* * FABRIC-0.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 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_quotas[slot].free_count--; 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; resident_list_push(slot, idx); return idx; } /* FABRIC-3.md aarch64 Stadium/COOL scaling fix: this used to be a * for (i = 0; i < stadium_ncells; i++) scan of the ENTIRE global array, * filtered down to this VM's own owned cells by an `owner[i] != slot` * check inside the loop -- O(stadium_ncells) on every call that reaches * here, not O(this VM's own resident count) as the function's own doc * above already claimed ("scoped to that SAME VM's own residents"). * Root-caused live (2026-09-11/12): word-execution admission * (stadium_word_dispatch(), stadium_words.c) calls stadium_admit() once * per distinct word a VM's own capsule executes, and each VM's granted * quota is a small, halving fraction of Hera's original pool -- once * that quota's free list empties (a handful of words in), EVERY further * new word this VM ever executes hit this scan. At stadium_ncells in * the hundreds of thousands (aarch64's -m 4096 vs. amd64/riscv64's * -m 1024 --> a 4x larger kmalloc heap --> a 4x larger Stadium, see * FABRIC-3.md SXV/SXVI), repeated hundred-thousand-cell scans compounded * into the observed 90+ minute stall. Walking this VM's own * resident_head list instead bounds the cost by how many cells this VM * actually owns (<= its granted quota, itself <= a small constant * multiple of DICTIONARY_SIZE in practice), independent of * stadium_ncells. */ for (idx = stadium_quotas[slot].resident_head; idx != STADIUM_CELL_NONE; idx = link_to_size(stadium_resident_next[idx])) { StadiumPatronHeader *h = &stadium_cell_array[idx].header; if (h->flags & STADIUM_FLAG_PIN) continue; if (h->contains != STADIUM_CONTAINS_NONE) continue; { uint64_t d = stadium_density(idx); if (least_dense_index == STADIUM_CELL_NONE || d < least_dense_value) { least_dense_index = idx; 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_quotas[slot].free_count--; 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; resident_list_push(slot, idx); return idx; } /* * FABRIC-0.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. from_slot's free-list length is now read directly from its * O(1)-maintained free_count (FABRIC-3.md SXVI donor-floor fix) rather than * walked -- the walk below only 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 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; half = stadium_quotas[from_slot].free_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[from_slot].free_count -= half; 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].free_count = half; stadium_quotas[new_slot].resident_head = STADIUM_CELL_NONE; stadium_quotas[new_slot].reservoir = Q48_ONE; return 0; } /* * stadium_best_donor - FABRIC-3.md SXVI donor-floor fix: which currently * in-use VM has the most free (unclaimed) cells right now. Callers granting * a new VM's initial quota (capsule_birth.c) should split from THIS VM, not * unconditionally from Hera -- the old always-Hera policy meant her own free * list converged toward empty after a bounded number of grants (each grant * halves what remains) while every VM she had already granted to typically * still held almost all of its own share untouched, silently starving every * later birth even though the Stadium as a whole was nowhere near full. * O(stadium_max_vm_count_val) -- a linear scan over quota SLOTS (bounded by * live VM population, not cell count), not the cells themselves. * * @return The VM with the largest free_count, or vm_uuid_none() if no VM * holds a quota yet (Stadium not initialized, or called before * Hera's own boot-time grant). */ VMUuid stadium_best_donor(void) { size_t i; size_t best_count = 0; int found = 0; VMUuid best = vm_uuid_none(); if (!stadium_initialized) return best; for (i = 0; i < stadium_max_vm_count_val; i++) { if (!stadium_quotas[i].in_use) continue; if (!found || stadium_quotas[i].free_count > best_count) { best_count = stadium_quotas[i].free_count; best = stadium_quotas[i].vm_id; found = 1; } } return best; } /* 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-0.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-2.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__ */