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>
353 lines
13 KiB
C
353 lines
13 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_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__ */
|