starkernel: REPL scrollback, ~1000 lines (FABRIC.md item 4.4q)
Scope decided with Captain Bob before implementation: ring buffer + recall on today's full-screen vt100 grid, not also confining REPL text to the 4.4o 640x480 box (that confinement stays open as its own future item, not a third silent deferral). No keyboard input path exists yet (M8 unstarted), so the trigger is two new FORTH words, SCROLL-BACK ( n -- ) / SCROLL-FWD ( n -- ), exercised via serial injection. vt100.c gains a text-only 1000-line ring buffer (kmalloc'd, tens of KB -- not pixel snapshots, which would be ~1000x larger for no benefit) plus a shadow buffer mirroring the current screen. scroll_up() now pushes evicted rows into the ring before the pixel scroll. History is one continuous sequence (ring then shadow); scrolling always redraws from that sequence -- no separate pixel-scroll path for scrollback, decided up front to avoid retrofitting later. New src/word_source/scroll_words.c (Module 31), thin wrappers over console_fb_scroll_back()/_fwd() -> vt100_scroll_back()/_fwd(). Bug caught during live testing: both words initially used an off-by-one underflow check (dsp < 1) copied from a different, older dsp convention elsewhere in this codebase; vm_pop() (which these words actually call) uses dsp as a 0-based top-of-stack index, so the check rejected every legitimate single-argument call. Fixed by removing the separate precheck and relying on vm_pop()'s own guard. Live-verified on all three architectures (exceeds this item's amd64-minimum bar): generated 50+ lines via a FORTH loop, confirmed SCROLL-BACK recovers correctly older content, and on amd64 confirmed SCROLL-FWD returns to genuinely live state (not a frozen snapshot) by showing the injected commands' own echo. Known limitation confirmed by direct pixel measurement: redrawn lines lose their original SGR color (not stored per-cell) -- text recovers exactly, color does not. Three-arch verified: Failed: 0, dict-hashes identical across all three (values changed correctly from prior items -- two new words were added). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
277845e3bf
commit
1f3ec3554e
@@ -303,6 +303,20 @@ void console_fb_enable_ttf(void)
|
||||
}
|
||||
}
|
||||
|
||||
void console_fb_scroll_back(uint32_t n)
|
||||
{
|
||||
if (fb_is_available()) {
|
||||
vt100_scroll_back(n);
|
||||
}
|
||||
}
|
||||
|
||||
void console_fb_scroll_fwd(uint32_t n)
|
||||
{
|
||||
if (fb_is_available()) {
|
||||
vt100_scroll_fwd(n);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single character from serial console (non-blocking)
|
||||
* Returns -1 if no character available
|
||||
|
||||
+178
-9
@@ -57,13 +57,12 @@
|
||||
* FABRIC.md item 4.4j: TTF glyph backend, active only from
|
||||
* vt100_enable_ttf() onward (boot/POST stays font_8x16.c).
|
||||
*
|
||||
* Provisional point size and cell geometry -- item 4.4m owns the final
|
||||
* REPL-strip decision. Chosen so 4.4i's confirmed hmtx result (all
|
||||
* Point size and cell geometry, decided final by item 4.4m (20px text,
|
||||
* 96px REPL strip = 4 lines). Chosen so 4.4i's confirmed hmtx result (all
|
||||
* printable ASCII share one 600/1000-em advance width) divides evenly:
|
||||
* 600/1000 * 20 = 12px exactly, no fractional-pixel cell width. Cell
|
||||
* height is size + a fixed descender/leading allowance, not derived from
|
||||
* hhea (not read by this parser -- see ttf.h) -- provisional like the
|
||||
* width.
|
||||
* hhea (not read by this parser -- see ttf.h).
|
||||
* --------------------------------------------------------------------- */
|
||||
#define VT100_TTF_SIZE_PX 20u
|
||||
#define VT100_TTF_CELL_W_PX 12u /* 600/1000 * VT100_TTF_SIZE_PX */
|
||||
@@ -81,6 +80,52 @@ static ttf_font_t g_ttf_font;
|
||||
static int g_ttf_ready = 0;
|
||||
static ttf_raster_cache_t g_ttf_cache;
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
* FABRIC.md item 4.4q: REPL scrollback. Text-only (not pixel snapshots --
|
||||
* at 1000 lines x ~cols bytes this is tens of KB; a pixel-snapshot ring
|
||||
* would be roughly three orders of magnitude larger for no benefit, since
|
||||
* the only consumer is a redraw). Only active in TTF mode (allocated in
|
||||
* vt100_enable_ttf(), never during boot/POST) -- font_8x16-mode boot
|
||||
* output has no scrollback and none is scoped for it.
|
||||
*
|
||||
* Scope, decided with Captain Bob 2026-08-11: this is scrollback on
|
||||
* today's full-screen terminal grid, NOT scoped to confine REPL text to
|
||||
* the 4.4o 640x480 box -- that confinement (vt100's grid still spans the
|
||||
* whole screen, not just the box) is a real, separate gap, observed by
|
||||
* 4.4p and still open; deliberately not fixed here, per that explicit
|
||||
* scoping decision rather than a silent deferral.
|
||||
*
|
||||
* Model: `g_shadow` mirrors exactly what's currently on screen (rows x
|
||||
* cols characters, updated by every draw_cursor_glyph() call and shifted
|
||||
* by every scroll_up()); `g_ring` holds every line scroll_up() has ever
|
||||
* evicted from the top of `g_shadow`, oldest-first, circular once full.
|
||||
* Conceptually one continuous history: ring (oldest..newest evicted) then
|
||||
* shadow (oldest..newest on-screen) back-to-back; g_scroll_offset is how
|
||||
* many lines back from the live (shadow) tail the current view sits.
|
||||
* Scrolling is always "pick a window into that history and redraw it" --
|
||||
* no separate pixel-scroll code path for scrollback, so recall is
|
||||
* trivially consistent with what was actually printed instead of a second
|
||||
* mechanism bolted onto fb_scroll_rows().
|
||||
*
|
||||
* Known limitations, not fixed here: redraw uses the terminal's current
|
||||
* default fg/bg for every recalled line -- per-cell historical SGR color
|
||||
* isn't stored, so scrollback recovers text exactly but not color.
|
||||
* Drawing new live output while g_scroll_offset > 0 is unverified and not
|
||||
* specially handled -- draw_cursor_glyph() always draws at the live
|
||||
* cursor position regardless of scroll offset, so it would visually
|
||||
* corrupt a scrolled-back view; out of scope for this item's verification
|
||||
* path (sequential FORTH-word injection over serial, never concurrent
|
||||
* with a live typist -- that's M8's keyboard-input problem, unstarted).
|
||||
* --------------------------------------------------------------------- */
|
||||
#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 uint32_t g_ring_head = 0; /* ring slot index of the oldest stored line */
|
||||
static uint32_t g_scroll_offset = 0; /* 0 = live view */
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
* Parser state enum
|
||||
* --------------------------------------------------------------------- */
|
||||
@@ -204,6 +249,12 @@ static void draw_cursor_glyph(uint8_t ch)
|
||||
uint32_t f = eff_fg();
|
||||
uint32_t b = eff_bg();
|
||||
|
||||
/* FABRIC.md item 4.4q: mirror every drawn character into the
|
||||
* scrollback shadow buffer, when active. */
|
||||
if (g_shadow && g_vt.cy < g_vt.rows && g_vt.cx < g_vt.cols) {
|
||||
g_shadow[(size_t)g_vt.cy * g_line_stride + g_vt.cx] = (char)ch;
|
||||
}
|
||||
|
||||
if (g_vt.bold) {
|
||||
/* Lighten fg for bold */
|
||||
uint32_t r = FB_R(f);
|
||||
@@ -311,15 +362,99 @@ void vt100_enable_ttf(void)
|
||||
|
||||
g_glyph_mode = VT_GLYPH_TTF;
|
||||
|
||||
/* Cell geometry just changed (font_8x16's fixed 8x16 vs. TTF's
|
||||
* provisional 12x24, see this file's 4.4j block above) -- re-derive
|
||||
* cols/rows and clear, the same as vt100_init() itself does. This only
|
||||
* ever runs once, at the POST-to-REPL boundary (sk_repl()), so nothing
|
||||
* of value is on screen yet to preserve. */
|
||||
/* Cell geometry just changed (font_8x16's fixed 8x16 vs. TTF's 12x24,
|
||||
* see this file's 4.4j block above) -- re-derive cols/rows and clear,
|
||||
* the same as vt100_init() itself does. This only ever runs once, at
|
||||
* the POST-to-REPL boundary (sk_repl()), so nothing of value is on
|
||||
* screen yet to preserve. */
|
||||
g_vt.cols = fb_width() / cell_w();
|
||||
g_vt.rows = fb_height() / cell_h();
|
||||
g_vt.cx = g_vt.cy = 0;
|
||||
erase_display(2);
|
||||
|
||||
/* FABRIC.md item 4.4q: allocate the scrollback shadow + ring buffers
|
||||
* now that cols/rows are fixed for the rest of this session. Failure
|
||||
* is non-fatal -- scrollback just stays unavailable (g_shadow/g_ring
|
||||
* stay NULL, every scrollback call site already checks), REPL text
|
||||
* output itself is unaffected either way. */
|
||||
{
|
||||
uint32_t i;
|
||||
g_line_stride = g_vt.cols + 1u;
|
||||
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);
|
||||
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; }
|
||||
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;
|
||||
g_scroll_offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
static const char *scrollback_line_at(uint32_t idx)
|
||||
{
|
||||
if (idx < g_ring_count) {
|
||||
uint32_t slot = (g_ring_head + idx) % VT100_SCROLLBACK_MAX_LINES;
|
||||
return g_ring + (size_t)slot * g_line_stride;
|
||||
} else {
|
||||
uint32_t row = idx - g_ring_count;
|
||||
if (row >= g_vt.rows) row = g_vt.rows - 1; /* defensive clamp */
|
||||
return g_shadow + (size_t)row * g_line_stride;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
static void scrollback_redraw(void)
|
||||
{
|
||||
uint32_t total = g_ring_count + g_vt.rows;
|
||||
uint32_t row;
|
||||
|
||||
for (row = 0; row < g_vt.rows; row++) {
|
||||
uint32_t idx = total - g_vt.rows - g_scroll_offset + row;
|
||||
const char *text = scrollback_line_at(idx);
|
||||
uint32_t col;
|
||||
|
||||
for (col = 0; col < g_vt.cols; col++) {
|
||||
uint8_t ch = (uint8_t)(text[col] ? text[col] : ' ');
|
||||
if (g_glyph_mode == VT_GLYPH_TTF) {
|
||||
ttf_draw_glyph_cell(px_of(col), py_of(row), ch, g_vt.def_fg, g_vt.def_bg);
|
||||
} else {
|
||||
fb_draw_glyph(px_of(col), py_of(row), ch, g_vt.def_fg, g_vt.def_bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void vt100_scroll_back(uint32_t n)
|
||||
{
|
||||
if (!g_shadow || !g_ring) return;
|
||||
if (n > g_ring_count - g_scroll_offset) n = g_ring_count - g_scroll_offset;
|
||||
if (n == 0) return;
|
||||
g_scroll_offset += n;
|
||||
scrollback_redraw();
|
||||
}
|
||||
|
||||
void vt100_scroll_fwd(uint32_t n)
|
||||
{
|
||||
if (!g_shadow || !g_ring) return;
|
||||
if (n > g_scroll_offset) n = g_scroll_offset;
|
||||
if (n == 0) return;
|
||||
g_scroll_offset -= n;
|
||||
scrollback_redraw();
|
||||
}
|
||||
|
||||
uint32_t vt100_cols(void) { return g_vt.cols; }
|
||||
@@ -350,8 +485,42 @@ static void advance_cursor(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* FABRIC.md item 4.4q: push the rows about to be discarded into the
|
||||
* scrollback ring before the pixel scroll happens, then shift the shadow
|
||||
* buffer to match. `n` is clamped to g_vt.rows -- scrolling further than
|
||||
* a full screen at once cannot happen through this code path (advance_cursor
|
||||
* only ever calls scroll_up(1)), but the clamp keeps this correct even if
|
||||
* a future caller passes more. */
|
||||
static void scroll_up(uint32_t lines)
|
||||
{
|
||||
if (g_shadow && g_ring && lines > 0) {
|
||||
uint32_t n = lines > g_vt.rows ? g_vt.rows : lines;
|
||||
uint32_t i, r;
|
||||
|
||||
for (r = 0; r < n; r++) {
|
||||
uint32_t slot = (g_ring_head + g_ring_count) % VT100_SCROLLBACK_MAX_LINES;
|
||||
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) {
|
||||
g_ring_count++;
|
||||
} else {
|
||||
g_ring_head = (g_ring_head + 1) % VT100_SCROLLBACK_MAX_LINES;
|
||||
}
|
||||
}
|
||||
|
||||
for (r = 0; r + n < g_vt.rows; r++) {
|
||||
char *dst = g_shadow + (size_t)r * g_line_stride;
|
||||
char *src = g_shadow + (size_t)(r + n) * g_line_stride;
|
||||
for (i = 0; i < g_line_stride; i++) dst[i] = src[i];
|
||||
}
|
||||
for (r = g_vt.rows - n; r < g_vt.rows; r++) {
|
||||
char *row = g_shadow + (size_t)r * g_line_stride;
|
||||
for (i = 0; i < g_vt.cols; i++) row[i] = ' ';
|
||||
row[g_vt.cols] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
fb_scroll_rows(lines, g_vt.def_bg);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user