Replaces STADIUM_MAX_VM_COUNT (Kconfig, hardcoded default 4) with a boot-time computation, mirroring the pattern stadium_boot_init() already used for the cell pool. New Kconfig STADIUM_VM_MEMORY_PERCENT (default 50): max_vm_count = (kmalloc_get_stats().free_bytes after the cell array * STADIUM_VM_MEMORY_PERCENT / 100) / VM_MEMORY_SIZE, floored to 1, no ceiling (population is not knowable in advance - could be 4, could be 4000). stadium_quotas and word_slots (plus stat_promotions/stat_evictions) are now kmalloc'd to the computed count instead of declared with a macro. New accessor stadium_max_vm_count() replaces every STADIUM_MAX_VM_COUNT reference, including capsule_birth.c's birth-refusal gate. Two things found and fixed along the way: - The existing cell-pool budget was sourced from pmm_get_stats(), which reflects physical pages PMM hasn't handed to any subsystem yet - but the actual allocation is kmalloc(), which draws from the separate, fixed-size heap kmalloc_init() (M6) already carved out of PMM before stadium_boot_init() ever runs. Budgeting against PMM's leftover and allocating from the kmalloc heap are two different pools. Both the cell budget and the new VM-count budget now source from kmalloc_get_stats() instead. - stadium_owner[] (which VM's quota owns each cell) was uint8_t, capped at 255 slots by a compile-time assert tied to the old macro. Widened to uint16_t (65535 slots of headroom) with a runtime clamp + log if the computed count ever exceeds that, since there's no ceiling anymore. Three-arch QEMU acceptance: all clean to ok>, computed VM count genuinely differs by actual available RAM (amd64/riscv64: 50 slots at -m 1024, aarch64: 101 slots), Stadium conservation invariant identical across all three (resident_sum=43691 reservoir=21845 sum=65536). logs/20260815-080526/amd64, logs/20260815-080826/aarch64, logs/20260815-080952/riscv64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
525 lines
26 KiB
C
525 lines
26 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.h - The Stadium cell and header (FABRIC.md §3, punch list item 3.1)
|
||
*
|
||
* A cell is one of exactly two things: a patron header, or a continuation
|
||
* cell owned by exactly one patron. The union is closed, two-valued, and
|
||
* fixed at build time -- not a type field. See FABRIC.md §3.
|
||
*/
|
||
|
||
#ifndef STARKERNEL_VM_STADIUM_H
|
||
#define STARKERNEL_VM_STADIUM_H
|
||
|
||
#ifdef __STARKERNEL__
|
||
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
#include "starforth_config.h" /* STADIUM_CONTAINS_DEPTH_MAX, STADIUM_CAPACITY_TICK, STADIUM_MEMORY_PERCENT */
|
||
#include "starkernel/vm_uuid.h" /* VMUuid -- FABRIC.md item 3.8 */
|
||
|
||
#define STADIUM_CELL_BYTES 64
|
||
|
||
/* Sentinel for `contains` meaning "holds no patron." Not 0 -- cell index 0 is
|
||
* a valid index (Hera, item 3.6), so 0 cannot double as "none" without
|
||
* conflating "contains Hera" with "contains nothing." */
|
||
#define STADIUM_CONTAINS_NONE ((uint32_t)-1)
|
||
|
||
/*
|
||
* StadiumPatronHeader - one member of the closed two-valued cell union
|
||
* (FABRIC.md §3). Nine wires: identity, heat, TTL, pin (a bit in `flags`),
|
||
* link, code field (`behaviour`), mass, payload, contains. `flags` bit 0 is
|
||
* `pin`; the remaining bits are reserved. `behaviour` is the closed code-field
|
||
* enumeration (§18.3) -- not yet defined, item 3.3's scope.
|
||
*
|
||
* Field order is largest-to-smallest so natural C99 alignment adds zero
|
||
* padding: every offset below is already a multiple of that field's own
|
||
* alignment, and the struct's total size (64) is a multiple of its max
|
||
* alignment (8), so no compiler inserts trailing padding either. Do not
|
||
* reorder without re-checking this holds on all three ISAs.
|
||
*/
|
||
typedef struct {
|
||
uint64_t identity; /* offset 0 -- handle or name, never a content hash while resident (§24.4) */
|
||
uint64_t heat; /* offset 8 -- Q48.16, conserved share of 1.0 (§19.1) */
|
||
uint32_t ttl; /* offset 16 -- remaining lifetime; messages and ACLs only (§17.1) */
|
||
uint32_t link; /* offset 20 -- index into the Stadium, not a pointer */
|
||
uint32_t contains; /* offset 24 -- index of the patron held inside this one, or
|
||
* STADIUM_CONTAINS_NONE (item 1.1). Cell index 0 is a valid
|
||
* index (Hera, item 3.6) so 0 cannot mean "none" -- item 3.5
|
||
* caught this and picked UINT32_MAX instead. Chains up to
|
||
* STADIUM_CONTAINS_DEPTH_MAX deep; reap-gating enforcement of
|
||
* that bound is item 3.5's scope. */
|
||
uint16_t mass; /* offset 28 -- cells this patron occupies (§19.2) */
|
||
uint8_t flags; /* offset 30 -- bit 0 = pin; remaining bits reserved */
|
||
uint8_t behaviour; /* offset 31 -- code field. Valid values are StadiumBehaviour (§18.3)
|
||
* tags cast to uint8_t -- kept as uint8_t rather than the enum type
|
||
* itself since C does not guarantee an enum's underlying type, and
|
||
* this field's offset is load-bearing for the 64-byte layout item
|
||
* 3.1 validated. */
|
||
uint8_t payload[32]; /* offset 32 -- inline payload, used when mass == 1 */
|
||
} StadiumPatronHeader;
|
||
|
||
/*
|
||
* StadiumContinuationCell - the other member of the union. Owned by exactly
|
||
* one patron header, chained by `next`. Never ranked, never reaped, never
|
||
* dispatched (§3) -- pure floor space, accounted for in its owner's mass.
|
||
*/
|
||
typedef struct {
|
||
uint32_t next; /* offset 0 -- index of the next continuation cell, or none */
|
||
uint8_t payload[60]; /* offset 4 */
|
||
} StadiumContinuationCell;
|
||
|
||
/*
|
||
* StadiumCell - the closed two-valued union itself (§3). Which member is
|
||
* valid for a given array slot is NOT stored in the cell -- FABRIC.md's item
|
||
* 3.1 amendment to §3 rules this an external side bitmap, one bit per cell,
|
||
* kept outside the cell array. Declared here as the indexing contract this
|
||
* type expects; item 3.2 (boot-time allocation) allocates the bitmap itself.
|
||
*/
|
||
typedef union {
|
||
StadiumPatronHeader header;
|
||
StadiumContinuationCell continuation;
|
||
} StadiumCell;
|
||
|
||
/* C99-portable compile-time size assertions (no _Static_assert -- that's C11). */
|
||
typedef char stadium_header_size_check[(sizeof(StadiumPatronHeader) == STADIUM_CELL_BYTES) ? 1 : -1];
|
||
typedef char stadium_continuation_size_check[(sizeof(StadiumContinuationCell) == STADIUM_CELL_BYTES) ? 1 : -1];
|
||
typedef char stadium_cell_size_check[(sizeof(StadiumCell) == STADIUM_CELL_BYTES) ? 1 : -1];
|
||
|
||
/*
|
||
* Items 1.1 and 1.4 named this item as where their Kconfig symbols would be
|
||
* implemented. Neither has a consumer yet (item 3.5 for the depth cap,
|
||
* capacity arbitration -- not yet on the punch list -- for the tick); these
|
||
* checks only prove the symbols are defined and sane, the same discipline
|
||
* already applied to the byte-count checks above.
|
||
*/
|
||
typedef char stadium_contains_depth_configured_check[(STADIUM_CONTAINS_DEPTH_MAX > 0) ? 1 : -1];
|
||
typedef char stadium_capacity_tick_configured_check[(STADIUM_CAPACITY_TICK > 0) ? 1 : -1];
|
||
|
||
/*
|
||
* Item 3.7, revised 2026-08-15: the per-cell owner array stores a quota-slot
|
||
* index. The VM population bound is no longer a compile-time constant (see
|
||
* stadium_max_vm_count() below), so this can no longer be a compile-time
|
||
* assert -- the owner element type is now uint16_t (65535 slots of
|
||
* headroom), and stadium_boot_init() itself clamps the computed count to
|
||
* that range at runtime, logging if it ever has to.
|
||
*/
|
||
|
||
/*
|
||
* stadium_boot_init - Boot-time allocation (FABRIC.md item 3.2, §17.6 position
|
||
* (b)). Sizes the global cell array from the memory budget actually observed
|
||
* at boot -- STADIUM_MEMORY_PERCENT of kmalloc_get_stats().free_bytes at the
|
||
* point of the call, rounded down to whole STADIUM_CELL_BYTES cells -- rather
|
||
* than a hardcoded count. (Corrected 2026-08-15 from pmm_get_stats(): PMM's
|
||
* free-byte figure reflects physical pages not yet handed to any subsystem,
|
||
* but kmalloc_init() (M6) already carved its own fixed-size heap out of PMM
|
||
* before this ever runs, and every allocation in this function actually
|
||
* draws from that kmalloc heap, not raw PMM -- pmm_get_stats() was budgeting
|
||
* against a pool nothing here actually allocates from.) Also computes the
|
||
* outer Stadium's VM population bound the same way, from the kmalloc heap's
|
||
* *remaining* free bytes after the cell array's own allocation: see
|
||
* stadium_max_vm_count() below. Also allocates the header/continuation
|
||
* discriminator bitmap item 3.1 declared but did not allocate: one bit per
|
||
* cell, bit set means the cell at that index is a patron header, clear means
|
||
* continuation or not yet in use. Both are kmalloc'd (freestanding kernel,
|
||
* no separate PMM-backed region needed for this) and explicitly zero-filled,
|
||
* since kmalloc does not zero.
|
||
*
|
||
* (Item 3.7) Also allocates a per-cell owner array (which VM's quota a cell
|
||
* belongs to; uint16_t as of 2026-08-15, see the note above) and chains
|
||
* every cell into a single free list, in ascending index order, granted in
|
||
* full to vm_id 0 (Hera) -- the only VM that exists (item 0.1). Ascending
|
||
* order guarantees the first-ever admission pops cell 0, preserving item
|
||
* 3.6's "Hera is patron zero" invariant once real birth-wiring calls
|
||
* stadium_admit() for the first time. The free-list next-pointer reuses
|
||
* each cell's own `link` field while unresident -- a repurposing of
|
||
* documented-but-unspecified storage, not a header change; see
|
||
* stadium_admit()'s doc for why this doesn't answer the separate,
|
||
* still-open continuation-chain question.
|
||
*
|
||
* Also allocates the VM quota array (stadium_quotas), sized to the
|
||
* computed stadium_max_vm_count() rather than a compile-time bound.
|
||
*
|
||
* Must be called after M6 (kmalloc_init) and before any VM is born (§6). Does
|
||
* not halt boot on failure -- nothing downstream consumes the Stadium yet.
|
||
*
|
||
* @return 0 on success, -1 if kmalloc failed for any of the four allocations.
|
||
*/
|
||
int stadium_boot_init(void);
|
||
|
||
/* stadium_is_initialized - Whether stadium_boot_init() has succeeded. */
|
||
int stadium_is_initialized(void);
|
||
|
||
/* stadium_cell_count - Number of cells in the array, 0 if not initialized. */
|
||
size_t stadium_cell_count(void);
|
||
|
||
/*
|
||
* stadium_max_vm_count - The outer Stadium's VM population bound, computed
|
||
* at stadium_boot_init() from the kmalloc heap's remaining free bytes
|
||
* (replaces the old compile-time STADIUM_MAX_VM_COUNT, 2026-08-15 -- see
|
||
* stadium_boot_init()'s own doc). 0 if not initialized. capsule_birth.c's
|
||
* birth gate reads this instead of a macro.
|
||
*/
|
||
size_t stadium_max_vm_count(void);
|
||
|
||
/* stadium_cells - Pointer to the cell array, NULL if not initialized. */
|
||
StadiumCell *stadium_cells(void);
|
||
|
||
/*
|
||
* stadium_header_bitmap - Pointer to the discriminator bitmap declared in
|
||
* item 3.1, NULL if not initialized. ceil(stadium_cell_count() / 8) bytes.
|
||
*/
|
||
uint8_t *stadium_header_bitmap(void);
|
||
|
||
/*
|
||
* StadiumBehaviour - the closed code-field enumeration (FABRIC.md §13, §18.3).
|
||
* The engine dispatches on this tag and never asks a patron what kind it is
|
||
* -- §3's entire point. Two patrons may share a tag: a VM's tag is COOL, the
|
||
* same tag a word carries (§18.3). Mapped from §17.1's patron table:
|
||
*
|
||
* MIGRATE -- blocks: reap event is migration back to Artemis (§17.2)
|
||
* DELIVER -- messages: reap event is delivery
|
||
* EXPIRE -- ACLs: reap event is TTL expiry
|
||
* COOL -- words and VMs: reap event is cooling off the floor
|
||
*
|
||
* Closed and fixed at build time -- see stadium_dispatch()'s exhaustive
|
||
* switch for how the compiler enforces that.
|
||
*/
|
||
typedef enum {
|
||
STADIUM_BEHAVIOUR_MIGRATE = 0,
|
||
STADIUM_BEHAVIOUR_DELIVER,
|
||
STADIUM_BEHAVIOUR_EXPIRE,
|
||
STADIUM_BEHAVIOUR_COOL
|
||
} StadiumBehaviour;
|
||
|
||
/*
|
||
* stadium_dispatch - Calls the behaviour handler for a patron's code field.
|
||
* The engine calls this at reap and never asks what kind of patron departed
|
||
* (§3, §18.3) -- only cell_index and behaviour cross this boundary.
|
||
*
|
||
* Handlers are stubs today: the real migrate-to-Artemis / deliver / expire /
|
||
* cool actions belong to their own subsystems, which have not been migrated
|
||
* onto the Stadium yet (Phase 4, §25.5). Nothing calls stadium_dispatch()
|
||
* yet either -- that begins with item 3.5 (admission and eviction).
|
||
*
|
||
* @param cell_index Index into the Stadium of the patron header dispatching.
|
||
* @param behaviour Which of the closed tag set to invoke.
|
||
*/
|
||
void stadium_dispatch(size_t cell_index, StadiumBehaviour behaviour);
|
||
|
||
/*
|
||
* stadium_density - Heat / mass for the patron header at cell_index (FABRIC.md
|
||
* §19.2, §19.3). Read, not computed by a scheduler: both operands already
|
||
* live in the header, so this is a division on demand, not maintained
|
||
* bookkeeping. Result stays valid Q48.16, since heat is already Q48.16 and
|
||
* mass is a plain integer divisor.
|
||
*
|
||
* Returns 0 if mass is 0 -- an empty or never-admitted slot (everything is
|
||
* zero-initialized by stadium_boot_init() until something is actually born
|
||
* into the Stadium, which nothing yet does) has no footprint to be dense
|
||
* within, rather than a division by zero.
|
||
*
|
||
* Does not validate that cell_index actually holds a header rather than a
|
||
* continuation cell or an out-of-range index -- callers are expected to
|
||
* consult the item-3.1 discriminator bitmap first. Ranking (finding the
|
||
* densest or least-dense resident) is item 3.5's scope, not this one's;
|
||
* this function only supplies the per-cell value that comparison reads.
|
||
*
|
||
* @param cell_index Index into the Stadium of the patron header to measure.
|
||
* @return Density in Q48.16, or 0 if the header's mass is 0.
|
||
*/
|
||
uint64_t stadium_density(size_t cell_index);
|
||
|
||
/* Sentinel returned by stadium_admit() on refusal -- no cell index is this large. */
|
||
#define STADIUM_CELL_NONE ((size_t)-1)
|
||
|
||
/*
|
||
* Hera is patron zero by construction of §6's boot order: she is the first
|
||
* entry admitted into the Stadium. This is a positional invariant, not a
|
||
* runtime check of who currently occupies cell 0 -- nothing yet births
|
||
* anything, Hera included, so today this index is never actually occupied.
|
||
* Used only by stadium_evict()'s item-3.6 assertion below.
|
||
*/
|
||
#define STADIUM_HERA_CELL_INDEX ((size_t)0)
|
||
|
||
/*
|
||
* stadium_birth_hera - Admits Hera as a real resident of cell 0 (FABRIC.md
|
||
* item 3.6's invariant, actually enforced -- item 4.1 found that nothing had
|
||
* ever called this until a word patron was about to become the first-ever
|
||
* occupant of cell 0 by accident via the free list). Candidate: identity 0,
|
||
* heat 0, mass 1, pinned (STADIUM_FLAG_PIN), behaviour COOL. Heat 0 means no
|
||
* reservoir transfer is needed -- conservation holds trivially (the
|
||
* reservoir keeps the VM's whole share; Hera's own cell contributes 0).
|
||
* Being pinned excludes her from every eviction-candidate scan (§3), so the
|
||
* stadium_evict() panic guard at STADIUM_HERA_CELL_INDEX stays correctly
|
||
* dormant rather than reachable-by-accident.
|
||
*
|
||
* Idempotent: a second call is a no-op (returns 0) if she is already
|
||
* resident. Must be called after stadium_boot_init() and before any word
|
||
* ever dispatches (§6) -- kernel_main.c calls it immediately after
|
||
* stadium_boot_init(), before M7's VM bootstrap.
|
||
*
|
||
* @return 0 on success (or already born), -1 if the Stadium is not
|
||
* initialized or the admission was refused (should not happen: her
|
||
* quota is granted in full, empty, at stadium_boot_init()).
|
||
*/
|
||
int stadium_birth_hera(void);
|
||
|
||
/*
|
||
* stadium_reservoir_pull - Transfers up to `amount` (Q48.16) out of vm_id's
|
||
* reservoir (FABRIC.md §17.7's reservoir mechanism). Clamped to what the
|
||
* reservoir actually holds -- never goes negative, never invents heat.
|
||
* Returns the amount actually pulled, which may be less than requested (or
|
||
* 0, e.g. a drained reservoir or an unknown vm_id). Callers that go on to
|
||
* fail their own operation (e.g. a refused stadium_admit()) MUST push the
|
||
* pulled amount back via stadium_reservoir_push() to preserve
|
||
* Σ(resident heat) + reservoir == Q48_ONE across the failed attempt.
|
||
*
|
||
* @param vm_id Owning VM's id.
|
||
* @param amount Requested Q48.16 amount.
|
||
* @return Amount actually pulled (0..amount).
|
||
*/
|
||
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount);
|
||
|
||
/*
|
||
* stadium_reservoir_push - Credits `amount` (Q48.16) back into vm_id's
|
||
* reservoir. The other half of every reservoir transfer (§17.7): cooling
|
||
* returns heat here, a refused starter-grant rolls back here, and
|
||
* stadium_evict() credits a departing patron's remaining heat here before
|
||
* the cell returns to the free list -- the invariant is a transfer, never a
|
||
* reset. No-op if vm_id has no quota (caller contract; mirrors
|
||
* stadium_admit()'s silent refusal for the same case).
|
||
*
|
||
* @param vm_id Owning VM's id.
|
||
* @param amount Q48.16 amount to credit.
|
||
*/
|
||
void stadium_reservoir_push(VMUuid vm_id, uint64_t amount);
|
||
|
||
/*
|
||
* stadium_reservoir_peek - Read-only: vm_id's current reservoir balance
|
||
* (Q48.16), for diagnostics/conservation checks. Does not mutate state.
|
||
* Returns 0 for an unknown vm_id -- indistinguishable from a genuinely
|
||
* drained reservoir, same as stadium_reservoir_pull()'s 0 return; callers
|
||
* that need to tell those apart must already know whether vm_id has a
|
||
* quota (e.g. via the same check they'd use before calling stadium_admit()).
|
||
*
|
||
* @param vm_id Owning VM's id.
|
||
* @return Current reservoir balance, or 0 if vm_id has no quota.
|
||
*/
|
||
uint64_t stadium_reservoir_peek(VMUuid vm_id);
|
||
|
||
/*
|
||
* stadium_quota_slot_for_vm - Read-only: vm_id's quota slot index (0 to
|
||
* stadium_max_vm_count()-1), for callers outside stadium.c that need to key
|
||
* their own per-VM state the same way stadium.c's internal arrays already
|
||
* do (FABRIC.md §25.5 item 4.2 -- stadium_words.c's word_id -> cell_index
|
||
* map needs this to stop colliding across VMs; word_id is scoped per-VM,
|
||
* not globally unique, so a single shared map aliases different VMs' words
|
||
* onto each other's Stadium cells and reservoirs).
|
||
*
|
||
* @param vm_id VM to look up.
|
||
* @return Quota slot index, or -1 if vm_id holds no quota.
|
||
*/
|
||
int stadium_quota_slot_for_vm(VMUuid vm_id);
|
||
|
||
/*
|
||
* stadium_resident_sum - Read-only: sum of heat across every cell currently
|
||
* resident AND owned by vm_id's own quota (FABRIC.md §25.5 item 4.2 --
|
||
* boot diagnostics need this filtered per-VM once a second VM holds a
|
||
* quota; summing every resident cell regardless of owner, as the pre-4.2
|
||
* diagnostic did, mixes two VMs' conservation totals together).
|
||
* Returns 0 for an unknown vm_id, same convention as stadium_reservoir_peek().
|
||
*
|
||
* @param vm_id Owning VM's id.
|
||
* @return Sum of resident heat owned by vm_id (Q48.16), or 0 if vm_id has no quota.
|
||
*/
|
||
uint64_t stadium_resident_sum(VMUuid vm_id);
|
||
|
||
/*
|
||
* stadium_evict - Reap the patron header at cell_index (FABRIC.md §17.2:
|
||
* "reap means leaves the floor, not destroyed"). Dispatches its behaviour
|
||
* (§18.3), clears its item-3.1 discriminator bit, zeroes its header, and
|
||
* (item 3.7) returns the freed cell to the free list of whichever VM's
|
||
* quota it was drawn from -- looked up via the internal per-cell owner
|
||
* record, not passed by the caller.
|
||
*
|
||
* PANICS (does not return) if cell_index == STADIUM_HERA_CELL_INDEX and the
|
||
* cell is actually resident -- FABRIC.md §20.5 #3: Hera is pinned (§3), but
|
||
* pinning alone is a silent guarantee, and item 3.6 requires a hard
|
||
* assertion at the eviction site rather than relying on pin holding. This
|
||
* check runs BEFORE the pin/contains checks below, deliberately: if pin were
|
||
* ever wrongly cleared, the ordinary pin-refusal path would quietly return
|
||
* -1 instead of surfacing the break, defeating the point of a second,
|
||
* independent check. Selecting patron zero for eviction means the invariant
|
||
* is already broken; continuing would run the system without a governor.
|
||
*
|
||
* Refuses (returns -1, does not panic) if the header is pinned (`flags` bit
|
||
* 0, §3's invariance wire) or has a non-none `contains` (item 1.1: a patron
|
||
* holding another cannot be reaped, full stop). Also refuses for an
|
||
* out-of-range index or a cell whose discriminator bit is not set (nothing
|
||
* resident there to reap).
|
||
*
|
||
* @param cell_index Index of the patron header to reap.
|
||
* @return 0 on success, -1 if refused.
|
||
*/
|
||
int stadium_evict(size_t cell_index);
|
||
|
||
/*
|
||
* StadiumVMQuota - per-VM ownership of a subset of the global cell array
|
||
* (FABRIC.md §22.3, item 3.7: "each VM holds its own free-list head index
|
||
* into the global array"). Linearly searched by vm_id -- a VMUuid (item 3.8)
|
||
* can't be used as a direct array index anyway. Was a small, compile-time-
|
||
* bounded table (linear scan "costs nothing" at the old default of 4);
|
||
* since 2026-08-15 the table is sized at boot from stadium_max_vm_count()
|
||
* and could genuinely be large, so this scan is no longer assumed free --
|
||
* flagged here rather than silently carried forward as still-obviously-fine,
|
||
* though no algorithmic change was made in this pass. Not exposed outside
|
||
* stadium.c: nothing outside needs to inspect
|
||
* quota state directly yet. Slot emptiness is tracked by an internal
|
||
* `in_use` flag, not a vm_id sentinel value -- there is no unused vm_id bit
|
||
* pattern to reserve for it.
|
||
*/
|
||
|
||
/*
|
||
* stadium_admit - Place a candidate patron header into the Stadium, scoped
|
||
* to vm_id's quota (FABRIC.md §19.3, §22.3, item 3.7).
|
||
*
|
||
* Pops vm_id's free-list head first (O(1)) if non-empty. Only if that VM's
|
||
* free list is exhausted does this fall back to eviction -- scoped to that
|
||
* SAME VM's own resident patrons only (quota isolation: a VM's admission can
|
||
* never evict another VM's patron), finding the least-dense evictable
|
||
* resident (not pinned, not `contains`-gated -- per §3 and item 1.1) and
|
||
* evicting it via stadium_evict() only if the candidate is strictly denser
|
||
* (§19.3: "denser than," not "at least as dense as"). Otherwise refuses.
|
||
*
|
||
* Does not itself assert anything about which resident this turns out to be
|
||
* -- the item-3.6 rule that patron zero (Hera) must never actually be
|
||
* selected is a separate, later check at the eviction site.
|
||
*
|
||
* REFUSES if vm_id has no quota granted (only Hera, vm_uuid_hera(), has one
|
||
* today -- granted the entire array at stadium_boot_init(), since she is the
|
||
* only VM that exists per item 0.1). Granting quota to additional VMs, and
|
||
* transferring capacity between them, is capacity ARBITRATION -- item 1.3
|
||
* left "how much capacity moves per eligible transfer" explicitly open, so
|
||
* this item does not invent it. Only the boot-time all-to-Hera grant exists.
|
||
*
|
||
* REFUSES any candidate with mass != 1. A multi-cell patron (mass > 1, e.g.
|
||
* §23.3's 1024-byte block at mass 19) needs a continuation chain, and no
|
||
* header field is documented anywhere as carrying the index of a patron's
|
||
* first continuation cell -- `link` is described only as generic "index
|
||
* into the Stadium, not a pointer." This item repurposes `link` for a
|
||
* different, non-conflicting use (the free-list next-pointer, while a cell
|
||
* is unresident -- see stadium.c), but does not invent an answer to the
|
||
* continuation-chain question, which stays open. Item 3.5's refusal
|
||
* therefore stands exactly as it was.
|
||
*
|
||
* REQUIRES candidate->contains to be either STADIUM_CONTAINS_NONE or a valid
|
||
* index (< the current cell count) -- refuses otherwise. This does NOT catch
|
||
* a zero-initialized candidate that was meant to contain nothing: 0 is a
|
||
* valid index (Hera), so a caller that forgets to set `contains` explicitly
|
||
* to STADIUM_CONTAINS_NONE will admit a patron that reads as "contains
|
||
* Hera" and is therefore permanently un-evictable. There is no way to tell
|
||
* "meant to be 0" from "forgot to set it" from inside this function --
|
||
* callers must set every field, `contains` included.
|
||
*
|
||
* @param vm_id Owning VM's id (capsule_birth.c's registry). Allocation
|
||
* is scoped to this VM's own quota.
|
||
* @param candidate Header to admit. Copied into the winning cell as-is;
|
||
* caller fills in every field including mass and heat.
|
||
* @return The cell index admitted into, or STADIUM_CELL_NONE if refused
|
||
* (vm_id has no quota, mass != 1, invalid contains, that VM's
|
||
* quota full and candidate not denser than its least-dense
|
||
* evictable resident, or it has no evictable resident at all).
|
||
*/
|
||
size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate);
|
||
|
||
/*
|
||
* stadium_grant_quota - One-time initial quota grant for a newly born VM
|
||
* (FABRIC.md item 4.1a). NOT item 1.3's recurring capacity-transfer
|
||
* arbitration -- that mechanism (density-gradient-driven, per capacity-tick)
|
||
* stays unbuilt and its "how much moves" question stays open. This is the
|
||
* narrower, one-time event: the same shape as Hera's own whole-pool grant at
|
||
* stadium_boot_init(), just from an existing VM's free list instead of the
|
||
* boot-time global one.
|
||
*
|
||
* Splits from_vm_id's free list evenly by cell count (new VM gets the first
|
||
* half by list-walk order; from_vm_id keeps the remainder, including any odd
|
||
* cell). Reassigns stadium_owner[] for every cell that moves. Touches no
|
||
* resident cell on either side -- only free-list cells move, so from_vm_id's
|
||
* residents (including a pinned cell 0, if from_vm_id is Hera) are
|
||
* unaffected. Grants new_vm_id a fresh Q48_ONE reservoir -- NOT a fraction of
|
||
* from_vm_id's, since conservation is per-VM (see StadiumVMQuota.reservoir's
|
||
* doc in stadium.c), not a shared pool split across VMs.
|
||
*
|
||
* REFUSES (returns -1, does not crash) if: the Stadium is not initialized;
|
||
* new_vm_id already holds a quota; from_vm_id holds no quota; from_vm_id's
|
||
* free list has fewer than 2 cells (nothing to split); or no empty quota
|
||
* slot remains (stadium_max_vm_count() exhausted).
|
||
*
|
||
* @param new_vm_id The VM receiving a fresh quota. Must not already have one.
|
||
* @param from_vm_id The VM whose free list is split. Must already hold a quota.
|
||
* @return 0 on success, -1 if refused.
|
||
*/
|
||
int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id);
|
||
|
||
/*
|
||
* stadium_cell_heat_get - Read a resident cell's own heat (FABRIC.md item
|
||
* 4.2's fourth ruling). Requires cell_index to be resident AND owned by
|
||
* vm_id's quota -- returns 0 otherwise (out of range, not resident, or
|
||
* belongs to a different VM), same ambiguity-with-a-genuine-zero already
|
||
* accepted by stadium_reservoir_peek()'s doc: callers that need to
|
||
* distinguish "refused" from "actually zero" must already know the cell is
|
||
* theirs (e.g. from their own resident-cell tracking), same contract as
|
||
* every other implicit-self primitive here.
|
||
*
|
||
* @param vm_id Calling VM's own identity.
|
||
* @param cell_index Index of the resident patron header to read.
|
||
* @return The cell's current heat (Q48.16), or 0 if refused.
|
||
*/
|
||
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index);
|
||
|
||
/*
|
||
* stadium_cell_heat_set - Write a resident cell's own heat, reconciling the
|
||
* reservoir delta atomically (FABRIC.md item 4.2's fourth ruling). Same
|
||
* ownership requirement as stadium_cell_heat_get(). If new_heat is higher
|
||
* than the cell's current heat, pulls the exact difference from vm_id's own
|
||
* reservoir first -- refuses (returns -1, no mutation) if the reservoir
|
||
* cannot cover the full increase, never a partial credit that would invent
|
||
* heat. If new_heat is lower, pushes the exact difference back to the
|
||
* reservoir after writing. Equal is a no-op success. This is the only
|
||
* sanctioned way to change a resident cell's heat post-admission -- doing
|
||
* the reservoir accounting here, not leaving it to the FORTH caller, is the
|
||
* whole reason this primitive exists rather than a raw field poke.
|
||
*
|
||
* @param vm_id Calling VM's own identity.
|
||
* @param cell_index Index of the resident patron header to write.
|
||
* @param new_heat The heat value to set (Q48.16).
|
||
* @return 0 on success, -1 if refused (not owned/resident, or insufficient
|
||
* reservoir for an increase).
|
||
*/
|
||
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat);
|
||
|
||
#endif /* __STARKERNEL__ */
|
||
|
||
#endif /* STARKERNEL_VM_STADIUM_H */ |