Files
StarForth/src/starkernel/doe_region.c
T
Robert Allan JamesandClaude Sonnet 5 2da746c01c Build DOE-PERSIST-TRIAL: block-backed DoE campaign persistence, live-verified
FABRIC-3.md §XXXV.3/.4 designed this and it was never built -- two live
campaigns got launched relying entirely on a live serial-log tail before
this was caught. doe_region.c/.h mirrors log_region.c's own growable-ring
pattern (own devblock_from_top fence at 98, past log_region's ceiling at
97) to persist one trial-summary record per completed trial straight to
disk/artemis.img, surviving past QEMU exit with no host required.

Verified end to end: booted amd64, called DOE-PERSIST-TRIAL at the
console, clean BYE, then read the raw disk bytes back with QEMU fully
dead -- confirmed the exact values passed in. Wired into both
multiuser-doe.4th and per-isa-doe.4th's trial markers. Clean build, zero
new warnings, all 3 ISAs; mkcapsule --lint passes both edited capsules.

FABRIC-3.md §XXXV.10 documents the fix and the root-cause correction: two
campaigns were launched on an unbuilt persistence design before this was
caught and fixed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-17 16:43:46 -04:00

149 lines
6.2 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.
*/
/**
* doe_region.c - Growable per-trial DoE-campaign-summary ring on Artemis's
* disk. See starkernel/doe_region.h for the format/interface design.
*
* Mirrors log_region.c's own read/write/grow/evict shape exactly (same
* blk_meta_zone_read()/write() accessor, same control-header CRC
* discipline), minus the priority-eviction logic that doesn't apply here
* (no level concept for a trial summary -- plain FIFO eviction only).
*/
#include "starkernel/doe_region.h"
#include <stddef.h>
#include <string.h>
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
static uint64_t doe_region_ctrl_compute_crc(const doe_region_ctrl_t *ctrl) {
size_t crc_span = offsetof(doe_region_ctrl_t, hdr_crc);
return compute_crc64((const uint8_t *)ctrl, crc_span);
}
/* Reads the control header; returns 0 and *out populated only if magic,
* version, and CRC all check out. Any other result (blank, foreign,
* corrupt, or a read failure) is treated identically by the caller: "not
* yet initialized," matching log_region_ctrl_read()'s own discipline. */
static int doe_region_ctrl_read(doe_region_ctrl_t *out) {
if (blk_meta_zone_read(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1;
if (DOE_REGION_GET_MAGIC(out->magic) != (uint32_t)(DOE_REGION_MAGIC & 0xFFFFFFFFull)) return -1;
if (DOE_REGION_GET_VERSION(out->magic) != DOE_REGION_VERSION_0) return -1;
uint64_t want_crc = doe_region_ctrl_compute_crc(out);
if (want_crc != out->hdr_crc) return -1;
return 0;
}
static int doe_region_ctrl_write(doe_region_ctrl_t *ctrl) {
ctrl->hdr_crc = doe_region_ctrl_compute_crc(ctrl);
return blk_meta_zone_write(DOE_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1;
}
static void doe_region_ctrl_init_fresh(doe_region_ctrl_t *ctrl) {
memset(ctrl, 0, sizeof(*ctrl));
ctrl->magic = DOE_REGION_PACK(DOE_REGION_VERSION_0);
ctrl->devblocks = DOE_REGION_INITIAL_DEVBLOCKS;
ctrl->head_slot = 0;
ctrl->tail_slot = 0;
ctrl->record_count = 0;
}
/* Reads/writes the devblock holding slot_index (DOE_SLOTS_PER_DEVBLOCK
* slots per devblock), one devblock_from_top past the control header for
* every DOE_SLOTS_PER_DEVBLOCK slots. */
static uint32_t doe_region_slot_devblock_from_top(uint32_t slot_index) {
return DOE_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / DOE_SLOTS_PER_DEVBLOCK);
}
static int doe_region_write_slot(uint32_t slot_index, const doe_trial_slot_t *slot) {
uint8_t devblock_buf[4096];
uint32_t dft = doe_region_slot_devblock_from_top(slot_index);
uint32_t slot_in_devblock = slot_index % DOE_SLOTS_PER_DEVBLOCK;
/* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock
* granularity, and a devblock holds DOE_SLOTS_PER_DEVBLOCK slots, so a
* single-slot write must preserve its siblings. A read failure on a
* never-yet-written devblock (blank fence content) is not fatal here --
* proceed with a zeroed buffer, matching log_region_write_slot()'s own
* discipline. */
if (blk_meta_zone_read(dft, devblock_buf) != 0) {
memset(devblock_buf, 0, sizeof(devblock_buf));
}
memcpy(devblock_buf + (size_t)slot_in_devblock * DOE_SLOT_SIZE, slot, sizeof(*slot));
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
}
int doe_region_append(uint32_t run_id, uint32_t cfg, uint32_t rep, uint32_t nw,
uint32_t mode, uint32_t fail_count,
uint64_t fleet_k_q48, uint32_t fleet_conserved,
uint64_t switch_count_cumulative,
const char *isa, uint32_t isa_len) {
doe_region_ctrl_t ctrl;
if (doe_region_ctrl_read(&ctrl) != 0) {
doe_region_ctrl_init_fresh(&ctrl);
}
uint32_t total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
if (ctrl.record_count >= total_slots) {
if (ctrl.devblocks < DOE_REGION_MAX_DEVBLOCKS) {
/* Grow: new slots appear at indices [old_total_slots ..
* new_total_slots-1], nothing already written moves -- safe
* at any point in the ring's lifecycle (see log_region.c's
* own identical reasoning). */
ctrl.devblocks += DOE_REGION_GROWTH_INCREMENT;
if (ctrl.devblocks > DOE_REGION_MAX_DEVBLOCKS) ctrl.devblocks = DOE_REGION_MAX_DEVBLOCKS;
total_slots = ctrl.devblocks * DOE_SLOTS_PER_DEVBLOCK;
}
if (ctrl.record_count >= total_slots) {
/* Still full (growth ceiling already reached) -- plain FIFO
* eviction, no priority concept for a trial summary. */
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
ctrl.record_count--;
}
}
doe_trial_slot_t slot;
memset(&slot, 0, sizeof(slot));
slot.fleet_k_q48 = fleet_k_q48;
slot.switch_count_cumulative = switch_count_cumulative;
slot.run_id = run_id;
slot.cfg = cfg;
slot.rep = rep;
slot.nw = nw;
slot.mode = mode;
slot.fail_count = fail_count;
slot.fleet_conserved = fleet_conserved;
uint32_t isa_n = isa_len;
if (isa_n > DOE_SLOT_ISA_MAX) isa_n = DOE_SLOT_ISA_MAX;
if (isa && isa_n) memcpy(slot.isa, isa, isa_n);
if (doe_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1;
ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots;
if (ctrl.record_count < total_slots) ctrl.record_count++;
return doe_region_ctrl_write(&ctrl) == 0 ? 0 : -1;
}