FABRIC.md item 4.4ac: scrollback during boot/POST, not just from REPL

vt100.c: font_8x16/bitmap-mode boot output previously had no scrollback
at all -- g_shadow/g_ring were only allocated in vt100_enable_ttf(), so
POST/self-test/heartbeat text was gone the instant it scrolled off,
recoverable only from the serial log. Gives boot mode its own ring/
shadow pair (bitmap cell geometry, 4096-line capacity), frozen as a
snapshot the moment vt100_enable_ttf() switches to the TTF-geometry
pair, per the two-independent-rings design scoped with Captain Bob.
scrollback_line_at()/scrollback_redraw()/vt100_scroll_back() now walk
all four segments (boot ring, boot shadow, TTF ring, TTF shadow) as one
continuous history, so PgUp from the REPL reaches back through POST.

Three-arch QEMU boot + logs clean (amd64/aarch64/riscv64, no faults, no
dictionary/parity regressions). Visual verification that PgUp actually
recalls POST text still needs an interactive GTK screendump -- noted as
open in FABRIC.md, same pattern as 4.4ab's screendump.

Also includes BLOCK_MAP.md/artemis.img regenerated by these builds, and
the acceptance-boot logs (plus stray logs from an earlier QEMU-instance
collision during testing -- kept per repo convention, logs are audit
artifacts, not deleted).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-12 18:21:31 -04:00
co-authored by Claude Sonnet 5
parent 149dbc4c73
commit 3396e9a7b4
10 changed files with 51614 additions and 15 deletions
+125 -14
View File
@@ -144,12 +144,53 @@ static int g_terminal_visible = 1;
#define VT100_SCROLLBACK_MAX_LINES 1000u
static char *g_shadow = (void *)0; /* g_vt.rows * g_line_stride bytes */
static char *g_ring = (void *)0; /* VT100_SCROLLBACK_MAX_LINES * g_line_stride bytes */
static uint32_t g_line_stride = 0; /* cols + 1 (NUL terminator), fixed once TTF mode starts */
static uint32_t g_ring_count = 0; /* valid stored lines, <= VT100_SCROLLBACK_MAX_LINES */
static char *g_ring = (void *)0; /* g_ring_cap * g_line_stride bytes */
static uint32_t g_line_stride = 0; /* cols + 1 (NUL terminator) */
static uint32_t g_ring_count = 0; /* valid stored lines, <= g_ring_cap */
static uint32_t g_ring_head = 0; /* ring slot index of the oldest stored line */
static uint32_t g_ring_cap = 0; /* active ring capacity -- VT100_BOOT_SCROLLBACK_MAX_LINES
* pre-TTF, VT100_SCROLLBACK_MAX_LINES from vt100_enable_ttf()
* onward; see 4.4ac below */
static uint32_t g_scroll_offset = 0; /* 0 = live view */
/* -----------------------------------------------------------------------
* FABRIC.md item 4.4ac: boot-mode scrollback. font_8x16/bitmap-mode boot
* output (POST, capsule birth, self-tests, heartbeat) previously had no
* scrollback at all -- g_shadow/g_ring above were only ever allocated in
* vt100_enable_ttf(), so anything printed before the REPL took over was
* gone the moment it scrolled off-screen. This block gives boot mode its
* own ring/shadow pair, allocated in vt100_init() at the bitmap cell
* geometry (8x16), separate from the TTF-geometry pair above because the
* two modes have different cell sizes and therefore different
* g_line_stride -- reflowing one buffer's stored lines into the other's
* width was rejected as unnecessary complexity for a text-only recall
* (decided with Captain Bob 2026-08-12).
*
* g_boot_shadow/g_boot_ring are frozen (snapshotted, not appended to)
* the moment vt100_enable_ttf() runs: it points g_shadow/g_ring at fresh
* TTF-geometry buffers, so this file's four scrollback segments become,
* oldest to newest: boot ring, boot shadow (the last screenful of boot
* output, frozen as of the transition), TTF ring, TTF shadow (live).
* scrollback_line_at()/scrollback_redraw() walk all four as one
* continuous history, per Captain Bob's call that PgUp from the REPL
* should reach all the way back through POST, not stop at a seam.
*
* Capacity guessed, not measured: 4096 lines vs. the TTF ring's 1000.
* A real qemu-esp boot transcript observed 2026-08-12 ran to ~8,500
* lines of POST/self-test/heartbeat output -- 4096 covers roughly half
* of that on its own; closing the rest of the gap is expected to come
* from trimming that verbosity (a separate, still-open item), not from
* growing this buffer indefinitely. Revisit if that trim doesn't happen. */
#define VT100_BOOT_SCROLLBACK_MAX_LINES 4096u
static char *g_boot_shadow = (void *)0; /* frozen boot g_shadow, snapshotted at vt100_enable_ttf() */
static char *g_boot_ring = (void *)0; /* frozen boot g_ring */
static uint32_t g_boot_line_stride = 0;
static uint32_t g_boot_ring_count = 0;
static uint32_t g_boot_ring_head = 0;
static uint32_t g_boot_ring_cap = 0;
static uint32_t g_boot_rows = 0; /* g_vt.rows at snapshot time; 0 if boot alloc failed */
/* -----------------------------------------------------------------------
* Parser state enum
* --------------------------------------------------------------------- */
@@ -386,6 +427,34 @@ void vt100_init(void)
g_vt.initialized = 1;
erase_display(2);
/* FABRIC.md item 4.4ac: allocate boot-mode scrollback now, at the
* bitmap cell geometry just computed above. Non-fatal on failure, same
* pattern as vt100_enable_ttf()'s TTF allocation below: g_shadow/g_ring
* stay NULL, draw_cursor_glyph()/scroll_up() already gate on that and
* skip scrollback tracking, so boot text just isn't recoverable --
* exactly the pre-4.4ac behavior, not a new failure mode. */
{
uint32_t i;
g_line_stride = g_vt.cols + 1u;
g_ring_cap = VT100_BOOT_SCROLLBACK_MAX_LINES;
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
g_ring = (char *)kmalloc((size_t)g_ring_cap * g_line_stride);
if (!g_shadow || !g_ring) {
log_message(LOG_ERROR, "vt100: boot scrollback buffer allocation failed, unavailable");
if (g_shadow) { kfree(g_shadow); g_shadow = (void *)0; }
if (g_ring) { kfree(g_ring); g_ring = (void *)0; }
} else {
for (i = 0; i < g_vt.rows; i++) {
uint32_t c;
char *row = g_shadow + (size_t)i * g_line_stride;
for (c = 0; c < g_vt.cols; c++) row[c] = ' ';
row[g_vt.cols] = '\0';
}
}
g_ring_count = 0;
g_ring_head = 0;
}
}
void vt100_enable_ttf(void)
@@ -414,6 +483,24 @@ void vt100_enable_ttf(void)
g_ttf_ready = 1;
}
/* FABRIC.md item 4.4ac: freeze the boot-mode scrollback buffers (if
* any -- vt100_init() may have failed to allocate them) before they
* get overwritten below. g_vt.cols/rows/g_shadow/g_ring/g_line_stride/
* g_ring_count/g_ring_head/g_ring_cap still hold boot-mode values at
* this point, nothing below has touched them yet. g_boot_rows is only
* set non-zero when the snapshot is actually usable -- if boot alloc
* failed, g_shadow is already NULL and g_ring_count is already 0 (
* scroll_up() never touches the ring when g_shadow/g_ring are NULL),
* so leaving g_boot_rows at 0 keeps scrollback_line_at()'s combined
* index space consistent instead of pointing into a NULL buffer. */
g_boot_shadow = g_shadow;
g_boot_ring = g_ring;
g_boot_line_stride = g_line_stride;
g_boot_ring_count = g_ring_count;
g_boot_ring_head = g_ring_head;
g_boot_ring_cap = g_ring_cap;
g_boot_rows = g_shadow ? g_vt.rows : 0;
g_glyph_mode = VT_GLYPH_TTF;
/* Full-screen grid, TTF cell size -- see this file's header comment.
@@ -439,8 +526,9 @@ void vt100_enable_ttf(void)
{
uint32_t i;
g_line_stride = g_vt.cols + 1u;
g_ring_cap = VT100_SCROLLBACK_MAX_LINES;
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
g_ring = (char *)kmalloc((size_t)VT100_SCROLLBACK_MAX_LINES * g_line_stride);
g_ring = (char *)kmalloc((size_t)g_ring_cap * g_line_stride);
if (!g_shadow || !g_ring) {
log_message(LOG_ERROR, "vt100: scrollback buffer allocation failed, scrollback unavailable");
if (g_shadow) { kfree(g_shadow); g_shadow = (void *)0; }
@@ -459,13 +547,32 @@ void vt100_enable_ttf(void)
}
}
/* Fetch the text of combined-history index `idx` (0 = oldest ring entry,
* g_ring_count + g_vt.rows - 1 = newest shadow row) -- see this file's
* 4.4q block for the ring-then-shadow history model. */
/* Fetch the text of combined-history index `idx` (0 = oldest boot-ring
* entry, ... newest TTF-shadow row = live) -- four segments back to back:
* boot ring, boot shadow (frozen at the boot->TTF transition), TTF ring,
* TTF shadow (live). See 4.4ac's block above for why boot and TTF each
* keep their own ring/shadow pair (different cell geometry, different
* g_line_stride) instead of one reflowed buffer, and 4.4q's block for the
* ring-then-shadow model each pair follows individually. Only ever called
* once TTF mode is active (scrollback is wired to a keyboard command that
* doesn't exist before the REPL starts -- console.c's
* console_fb_scroll_back()/_fwd()), so g_ring/g_shadow here always mean
* the TTF pair, never the boot one. */
static const char *scrollback_line_at(uint32_t idx)
{
if (idx < g_boot_ring_count) {
uint32_t slot = (g_boot_ring_head + idx) % g_boot_ring_cap;
return g_boot_ring + (size_t)slot * g_boot_line_stride;
}
idx -= g_boot_ring_count;
if (idx < g_boot_rows) {
return g_boot_shadow + (size_t)idx * g_boot_line_stride;
}
idx -= g_boot_rows;
if (idx < g_ring_count) {
uint32_t slot = (g_ring_head + idx) % VT100_SCROLLBACK_MAX_LINES;
uint32_t slot = (g_ring_head + idx) % g_ring_cap;
return g_ring + (size_t)slot * g_line_stride;
} else {
uint32_t row = idx - g_ring_count;
@@ -476,10 +583,12 @@ static const char *scrollback_line_at(uint32_t idx)
/* Redraw every row from the combined history at the current
* g_scroll_offset. Uses the terminal's current default colors, not each
* line's original SGR state -- see this file's 4.4q block. */
* line's original SGR state -- see this file's 4.4q block. `total` now
* spans all four 4.4ac segments, not just the TTF ring + shadow, so
* scrolling back from the REPL reaches all the way through POST. */
static void scrollback_redraw(void)
{
uint32_t total = g_ring_count + g_vt.rows;
uint32_t total = g_boot_ring_count + g_boot_rows + g_ring_count + g_vt.rows;
uint32_t row;
for (row = 0; row < g_vt.rows; row++) {
@@ -534,8 +643,10 @@ void vt100_toggle_graphics(void)
void vt100_scroll_back(uint32_t n)
{
uint32_t history; /* 4.4ac: boot ring + boot shadow + TTF ring, not just the TTF ring */
if (!g_shadow || !g_ring) return;
if (n > g_ring_count - g_scroll_offset) n = g_ring_count - g_scroll_offset;
history = g_boot_ring_count + g_boot_rows + g_ring_count;
if (n > history - g_scroll_offset) n = history - g_scroll_offset;
if (n == 0) return;
g_scroll_offset += n;
scrollback_redraw();
@@ -591,14 +702,14 @@ static void scroll_up(uint32_t lines)
uint32_t i, r;
for (r = 0; r < n; r++) {
uint32_t slot = (g_ring_head + g_ring_count) % VT100_SCROLLBACK_MAX_LINES;
uint32_t slot = (g_ring_head + g_ring_count) % g_ring_cap;
char *dst = g_ring + (size_t)slot * g_line_stride;
char *src = g_shadow + (size_t)r * g_line_stride;
for (i = 0; i < g_line_stride; i++) dst[i] = src[i];
if (g_ring_count < VT100_SCROLLBACK_MAX_LINES) {
if (g_ring_count < g_ring_cap) {
g_ring_count++;
} else {
g_ring_head = (g_ring_head + 1) % VT100_SCROLLBACK_MAX_LINES;
g_ring_head = (g_ring_head + 1) % g_ring_cap;
}
}