Files
LithosAnanake/kernel/src/vm/stadium_blocks.c
T
rajamesandJunie a8b70e88d3 Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure
Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-01 15:40:09 -04:00

353 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
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_blocks.c - Block patrons on the Stadium (FABRIC-2.md §B/§D,
* MIGRATE punch-list item). See stadium_blocks.h for the design rationale.
*/
#include "starkernel/vm/stadium_blocks.h"
#ifdef __STARKERNEL__
#include "starkernel/vm/stadium.h"
#include "starkernel/console.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" /* Q48_ONE */
#include "starforth_config.h" /* STADIUM_BLOCK_HEAT_QUANTUM, STADIUM_BLOCK_COOL_RATE_Q48,
STADIUM_BLOCK_TRACK_CAP_MULT */
/*
* StadiumBlockSlot - one entry in the fixed-capacity open-addressing hash
* table. Keyed by (quota_slot, lbn) together -- LBN numbering is global
* across VMs (unlike word_id), but two VMs touching the SAME lbn must still
* land in different Stadium cells under different quotas, so both fields
* are part of the key (same lesson stadium_words.c's item 4.2 fix already
* applied for word_id).
*
* `state` is three-valued, not a bool: standard open-addressing deletion
* needs EMPTY vs. TOMBSTONE distinguished from OCCUPIED, or reclaiming a
* stale entry's slot (self-healing, see stadium_word_dispatch()'s identical
* problem for the dense word map) would silently break the probe chain for
* any other key that happened to hash into the same bucket and probed past
* this slot to find its own home.
*/
typedef enum {
BLOCK_SLOT_EMPTY = 0,
BLOCK_SLOT_OCCUPIED,
BLOCK_SLOT_TOMBSTONE
} StadiumBlockSlotState;
typedef struct {
uint8_t state;
int16_t quota_slot;
uint32_t lbn;
size_t cell_index;
uint64_t last_decay_tick;
} StadiumBlockSlot;
static StadiumBlockSlot *block_slots = (StadiumBlockSlot *)0;
static size_t block_slots_cap = 0;
static int blocks_initialized = 0;
/* Per-quota-slot counters, kmalloc'd to stadium_max_vm_count() entries --
* same reasoning as stadium_words.c's stat_promotions/stat_evictions: a
* single global counter can't answer "how is vm_id's own table doing"
* once more than one VM holds a quota. */
static uint64_t *stat_promotions = (uint64_t *)0;
static uint64_t *stat_evictions = (uint64_t *)0;
void stadium_blocks_init(void) {
size_t vm_count;
size_t cell_count;
size_t i;
if (blocks_initialized) return; /* not re-entrant -- see stadium_blocks.h */
cell_count = stadium_cell_count();
vm_count = stadium_max_vm_count();
if (cell_count == 0 || vm_count == 0) {
console_println("Stadium blocks: init skipped (Stadium not initialized)");
return;
}
block_slots_cap = cell_count * (size_t)STADIUM_BLOCK_TRACK_CAP_MULT;
block_slots = (StadiumBlockSlot *)kmalloc(block_slots_cap * sizeof(StadiumBlockSlot));
stat_promotions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
stat_evictions = (uint64_t *)kmalloc(vm_count * sizeof(uint64_t));
if (!block_slots || !stat_promotions || !stat_evictions) {
console_println("Stadium blocks: kmalloc failed for tracking tables");
if (block_slots) kfree(block_slots);
if (stat_promotions) kfree(stat_promotions);
if (stat_evictions) kfree(stat_evictions);
block_slots = (StadiumBlockSlot *)0;
block_slots_cap = 0;
stat_promotions = (uint64_t *)0;
stat_evictions = (uint64_t *)0;
return;
}
for (i = 0; i < block_slots_cap; i++) {
block_slots[i].state = BLOCK_SLOT_EMPTY;
block_slots[i].quota_slot = -1;
block_slots[i].lbn = 0;
block_slots[i].cell_index = STADIUM_CELL_NONE;
block_slots[i].last_decay_tick = 0;
}
for (i = 0; i < vm_count; i++) {
stat_promotions[i] = 0;
stat_evictions[i] = 0;
}
blocks_initialized = 1;
}
static int cell_is_resident(size_t idx) {
const uint8_t *bm = stadium_header_bitmap();
if (!bm) return 0;
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* block_hash - Multiplicative mix of (quota_slot, lbn) into [0, block_slots_cap).
* No libc hash dependency (freestanding kernel); Knuth's multiplicative
* method (constant 2654435761, the standard 32-bit golden-ratio prime) is
* enough to spread a monotonically-touched LBN sequence across buckets --
* this table's job is short probe chains under normal access patterns, not
* cryptographic distribution.
*/
static size_t block_hash(int16_t quota_slot, uint32_t lbn) {
uint32_t key = lbn ^ ((uint32_t)(uint16_t)quota_slot << 16) ^ ((uint32_t)(uint16_t)quota_slot);
key = key * 2654435761u;
return (size_t)(key % (uint32_t)block_slots_cap);
}
/*
* table_probe - Single linear-probe pass serving both lookup and insertion.
* Walks at most block_slots_cap slots starting at the key's hash bucket.
*
* - If an OCCUPIED slot matching (quota_slot, lbn) is found: returns its
* index, *out_found = 1.
* - Else, if any EMPTY or TOMBSTONE slot was seen along the way: returns
* the FIRST such slot's index (so insertion reuses the earliest opening,
* standard open-addressing practice), *out_found = 0.
* - Else (table genuinely full, no opening and no match): returns
* block_slots_cap, *out_found = 0.
*
* Probing must continue past TOMBSTONE slots when searching for a match --
* a tombstone marks "something used to live here," not "the chain ends
* here," or a live entry further down the same chain would become
* unreachable.
*/
static size_t table_probe(int16_t quota_slot, uint32_t lbn, int *out_found) {
size_t start = block_hash(quota_slot, lbn);
size_t first_open = block_slots_cap; /* sentinel: none seen yet */
size_t i;
for (i = 0; i < block_slots_cap; i++) {
size_t idx = (start + i) % block_slots_cap;
StadiumBlockSlot *s = &block_slots[idx];
if (s->state == BLOCK_SLOT_EMPTY) {
if (first_open == block_slots_cap) first_open = idx;
break; /* empty slot ends any possible chain for this key */
}
if (s->state == BLOCK_SLOT_TOMBSTONE) {
if (first_open == block_slots_cap) first_open = idx;
continue;
}
/* OCCUPIED */
if (s->quota_slot == quota_slot && s->lbn == lbn) {
*out_found = 1;
return idx;
}
}
*out_found = 0;
return first_open; /* block_slots_cap if truly full */
}
/*
* block_dispatch_pull - Reservoir pull for block-touch admission, same
* Q48_ONE / 3 floor stadium_word_dispatch()'s word_dispatch_pull() enforces
* and for the identical reason (FABRIC-0.md §25.5/§25.7): block-touch
* admission alone must never be able to drain a VM's reservoir below the
* floor other reservoir consumers (word execution, application-level
* pulls) depend on. Own copy, not shared code across files -- same
* decision stadium_words.c's own version already made.
*/
static uint64_t block_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
void stadium_block_dispatch(VMUuid vm_id, uint32_t lbn, uint64_t heartbeat_ticks) {
int slot;
int found;
size_t idx;
int stale = 0;
if (!blocks_initialized) return;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
idx = table_probe((int16_t)slot, lbn, &found);
if (found) {
StadiumBlockSlot *bs = &block_slots[idx];
size_t cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) {
stale = 1;
} else if (stadium_cells()[cell].header.identity != (uint64_t)lbn) {
stale = 1;
}
if (!stale) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - bs->last_decay_tick;
if (elapsed > 0) {
uint64_t per_tick = (h->heat * (uint64_t)STADIUM_BLOCK_COOL_RATE_Q48) >> 16;
uint64_t cooled = per_tick * elapsed;
if (cooled > h->heat) cooled = h->heat;
if (cooled > 0) {
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
bs->last_decay_tick = heartbeat_ticks;
}
h->heat += block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
return;
}
/* Stale: reclaim this exact slot -- idx is still this key's home. */
bs->state = BLOCK_SLOT_TOMBSTONE;
stat_evictions[slot]++;
}
if (idx >= block_slots_cap) return; /* table full: skip tracking this touch */
{
uint64_t pulled = block_dispatch_pull(vm_id, (uint64_t)STADIUM_BLOCK_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
size_t cell_idx;
for (i = 0; i < sizeof(candidate); i++) raw[i] = 0;
candidate.identity = (uint64_t)lbn;
candidate.heat = pulled;
candidate.ttl = 0;
candidate.link = 0; /* unused for block patrons, same as words */
candidate.contains = STADIUM_CONTAINS_NONE;
candidate.mass = 1;
candidate.flags = 0; /* unpinned */
candidate.behaviour = (uint8_t)STADIUM_BEHAVIOUR_MIGRATE;
cell_idx = stadium_admit(vm_id, &candidate);
if (cell_idx == STADIUM_CELL_NONE) {
stadium_reservoir_push(vm_id, pulled); /* rollback: preserve conservation */
return;
}
block_slots[idx].state = BLOCK_SLOT_OCCUPIED;
block_slots[idx].quota_slot = (int16_t)slot;
block_slots[idx].lbn = lbn;
block_slots[idx].cell_index = cell_idx;
block_slots[idx].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
/* Freestanding: no libc printf. Same small utility stadium_words.c
* duplicates locally (and stadium.c before it) -- prints an unsigned
* decimal, no leading zeros. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
void stadium_blocks_print_boot_diagnostics(VMUuid vm_id) {
int slot = stadium_quota_slot_for_vm(vm_id);
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum = 0;
uint64_t reservoir;
size_t i;
if (!blocks_initialized || slot < 0) {
console_println("Stadium blocks: not initialized or vm_id holds no quota");
return;
}
promotions = stat_promotions[slot];
evictions = stat_evictions[slot];
/* Block-only resident sum: walk this VM's own table entries, same
* self-healing residency check stadium_block_dispatch() uses -- a
* stale entry contributes 0, not garbage. Not exposed as a separate
* API (unlike stadium_words_resident_heat()) since nothing else needs
* it yet; add one if a caller shows up. */
for (i = 0; i < block_slots_cap; i++) {
StadiumBlockSlot *bs = &block_slots[i];
size_t cell;
if (bs->state != BLOCK_SLOT_OCCUPIED || bs->quota_slot != (int16_t)slot) continue;
cell = bs->cell_index;
if (cell == STADIUM_CELL_NONE || cell >= stadium_cell_count() || !cell_is_resident(cell)) continue;
if (stadium_cells()[cell].header.identity != (uint64_t)bs->lbn) continue;
resident_sum += stadium_cells()[cell].header.heat;
}
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium blocks: promotions=");
console_put_u64(promotions);
console_puts(" evictions=");
console_put_u64(evictions);
console_println("");
console_puts("Stadium blocks resident_sum=");
console_put_u64(resident_sum);
console_puts(" reservoir=");
console_put_u64(reservoir);
console_println(" (block-only term, not a standalone Q48_ONE check -- reservoir is shared)");
}
#endif /* __STARKERNEL__ */