stadium: make VM population bound RAM-derived, not a static array of 4

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>
This commit is contained in:
Robert Allan James
2026-08-15 08:11:21 -04:00
co-authored by Claude Sonnet 5
parent 154eddeab0
commit 00e657019e
16 changed files with 26118 additions and 127 deletions
+68
View File
@@ -823,3 +823,71 @@ unreachable NULL-write, `m5_time_trust`/`m5_variance` dead fields.
closed items, four sequencing-blocked items (no ruling needed, just not startable yet), and
one code question. Nothing in either track's open half is closeable without either Artemis or
an explicit decision from Captain Bob — which is the state this pass was asked to produce.
---
## G. Stadium VM population bound made RAM-derived — 2026-08-15
Captain Bob flagged, mid-conversation, that `STADIUM_MAX_VM_COUNT` being a hardcoded
compile-time `4` was not the design he had in mind: "The stadium has capacity that is
determined at boot time with the remaining ram... a static array of four, then we're
fucked." Confirmed by grep: `STADIUM_MAX_VM_COUNT` (Kconfig default 4) sized two genuinely
static compile-time arrays — `stadium_quotas[STADIUM_MAX_VM_COUNT]` and
`word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE]` — and gated `capsule_birth.c`'s birth
refusal, with zero RAM-based computation anywhere near it. Not an oversight: `FABRIC.md`
§20.5 item 1.5 (RESOLVED 2026-08-04) explicitly decided this the other way — "fixed for the
machine's lifetime once set at build... the outer total does not itself flex at runtime."
Captain Bob overruled that: population is not knowable in advance (could be 4, could be
4000), so the bound must be computed at boot, no ceiling.
**What was already correct, and what wasn't.** The Stadium's *cell pool* (actual patron
storage) was already boot-time RAM-derived — `stadium_boot_init()`
(`src/starkernel/vm/stadium.c:118`) already computed `ncells` from a percentage of free
memory and `kmalloc()`'d it. Only the *VM population ceiling* was static.
**A second, real bug found while fixing this.** The cell-pool budget was computed from
`pmm_get_stats().free_bytes` — physical pages PMM hasn't handed to any subsystem yet — but
the actual allocation was `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; the percentage was being applied to memory nothing here actually draws from. Fixed
as part of this change — both the existing cell budget and the new VM-count budget now
source from `kmalloc_get_stats()`.
**Owner-byte width.** `stadium_owner[]` (which VM's quota owns each cell) was `uint8_t`,
capped at 255 slots by the old compile-time assert. With no ceiling, this needed widening —
done, `uint16_t` (65535 slots of headroom), with a runtime clamp + log if the computed count
ever exceeds that.
**Mechanism (mirrors the existing cell-pool pattern exactly):** new Kconfig
`STADIUM_VM_MEMORY_PERCENT` (default 50, untuned placeholder like its `STADIUM_MEMORY_PERCENT`
sibling). At boot, after the cell array is allocated: `max_vm_count =
(kmalloc_get_stats().free_bytes * STADIUM_VM_MEMORY_PERCENT / 100) / VM_MEMORY_SIZE`
(`VM_MEMORY_SIZE` = 5 MiB, the real per-VM footprint — not the small bookkeeping tables),
floored to 1 so Hera can always boot, clamped to 65535. `stadium_quotas` and `word_slots`
(plus their `stat_promotions`/`stat_evictions` companions) are now `kmalloc()`'d to that
count instead of declared with the macro. New accessor `stadium_max_vm_count()` replaces
every `STADIUM_MAX_VM_COUNT` reference, including `capsule_birth.c`'s birth-refusal gate.
**Files touched:** `Kconfig.kernel`, `include/starforth_config.h`,
`include/starkernel/vm/stadium.h`, `include/starkernel/vm/stadium_words.h`,
`include/starkernel/capsule_run.h` (comment only), `src/starkernel/vm/stadium.c`,
`src/starkernel/vm/stadium_words.c`, `src/starkernel/capsule/capsule_birth.c`.
**Verification, all three architectures clean, computed VM count genuinely differs across
runs (proof it's really reading RAM, not a disguised constant):**
- **amd64:** `Stadium: 83886 cells (5242 KB), 50 VM slots``logs/20260815-080526/amd64/`
- **aarch64:** `Stadium: 167772 cells (10485 KB), 101 VM slots``logs/20260815-080826/aarch64/`
- **riscv64:** `Stadium: 83886 cells (5242 KB), 50 VM slots``logs/20260815-080952/riscv64/`
All three reached `ok>` clean with Hermes's self-test, an identical Stadium conservation
check (`resident_sum=43691 reservoir=21845 sum=65536`, `Q48_ONE=65536`), and KILL/rest all
passing — the RAM-derived count changed, the physics invariant it feeds into didn't, which
is exactly what should happen. Up from the old fixed `4` in every case.
**Related, same conversation:** this surfaced alongside a correction to §12 Q5's
`STADIUM_CAPACITY_TICK` framing (F.2 above) — Captain Bob's "the clock is only the
heartbeat, period" pushback, confirmed against `FABRIC.md` §16.4/§17.1's decided one-clock
rule. Both are Stadium-capacity-adjacent but independent: this item is the VM *population*
bound (a count), that one is the fleet-capacity *cadence* (a tick threshold).